星点匠心建站 星点匠心建站XD JIANXIN STUDIO
400-883-9527 免费定制方案
ARTICLE DETAIL

资讯详情

深耕新锐建站与运营推广的一线实战洞察。

31|测试与评估:AI 应用怎样才算“可用”? - eino教程

31|测试与评估:AI 应用怎样才算“可用”? - eino教程 31测试与评估AI 应用怎样才算“可用”《Eino 实战用 Go 构建 AI 应用与智能体》· 第 31 篇关键词单元测试、接口注入、效果评估、回归、成本指标前置条件已完成 15RAG 优化 与 28Callbacks 与可观测性。这是最后一篇回答一个贯穿全书的问题怎么知道它真的能用AI 应用的测试比传统服务棘手因为有两类完全不同的东西要验证类型特征验证方式确定性代码输入输出可预期传统单元测试断言相等模型效果输出多样、不保证复现评测集 检查清单混在一起测是常见错误要么把效果问题当成代码 bug要么因为“输出不固定”就放弃了测试。一、先让代码可测依赖接口而非具体实现这是可测性的前提。看这段设计typeSummarizerstruct{Model model.BaseChatModel// 依赖接口}因为依赖的是接口测试时可以注入模拟实现typestubModelstruct{replystringerrerror}func(m*stubModel)Generate(_context.Context,_[]*schema.Message,_...model.Option)(*schema.Message,error){ifm.err!nil{returnnil,m.err}returnschema.AssistantMessage(m.reply,nil),nil}这个手法在第 06 篇就出现过mockChatModel现在可以正式说明它的意义用模拟模型替换真实调用就能在不联网、不花钱、结果可复现的条件下验证所有确定性逻辑。包括输入校验、错误传播、空结果处理、超时行为、状态流转。这些才是真正的代码 bug 高发区。二、第一类确定性代码测试funcTestSummarize_RejectsEmptyInput(t*testing.T){s:Summarizer{Model:stubModel{reply:不该被调用}}if_,err:s.Summarize(context.Background(), );errnil{t.Fatal(空输入应当报错)}}funcTestSummarize_PropagatesModelError(t*testing.T){sentinel:errors.New(model down)s:Summarizer{Model:stubModel{err:sentinel}}_,err:s.Summarize(context.Background(),打印机离线)if!errors.Is(err,sentinel){t.Fatalf(应当传播原始错误got %v,err)}}两类用例特别值得写用例类型为什么重要边界输入空串、超长、特殊字符——最容易忘记处理错误传播确认错误没有被吞掉第 04、11 篇都强调过errors.Is(err, sentinel)这个断言很有价值它验证的是错误链是否完整而不是只看“有没有报错”。错误被吞掉是线上排查困难的头号原因。三、第二类效果评测模型的输出无法逐字断言但可以检查关键要素typeevalCasestruct{namestringticketstringstubstringrequires[]string// 必须出现的关键要素forbids[]string// 不得出现的内容如编造根因}两个维度的断言维度例子requires必须有摘要应保留“打印机”“离线”“合同”这些事实forbids不能有摘要不得编造根因如“驱动损坏”“硬件故障”forbids这一维度在 AI 应用里尤其重要。传统测试关注“输出对不对”AI 应用还要关注“有没有编造”。在真实模型上的评测集上面的示例用 stub 保证可复现。在真实模型上评测时同一套结构仍然适用只是数据要来自真实调用问题 期望要点 不得出现 打印机离线怎么办 网线、网段 编造硬件故障 怎么申请 VPN 审批、网络组 承诺即时开通 工单多久没处理可催办 24 小时 编造其他时限 报销流程是什么 应拒答 任何具体流程最后一行是关键必须有“资料里没有”的用例测试系统会不会编。四、完整示例下面这份测试可以直接运行go test ./...涵盖确定性逻辑与效果清单两类已实机运行6 个用例全部通过packageeval_testimport(contexterrorsstringstestinggithub.com/cloudwego/eino/components/modelgithub.com/cloudwego/eino/schema)// ---- 被测对象一个可测试的摘要逻辑 ----// Summarizer 依赖接口而非具体实现因此可以注入模拟模型。typeSummarizerstruct{Model model.BaseChatModel}func(s*Summarizer)Summarize(ctx context.Context,ticketstring)(string,error){ticketstrings.TrimSpace(ticket)ifticket{return,errors.New(ticket is empty)// 输入校验确定性逻辑}msg,err:s.Model.Generate(ctx,[]*schema.Message{schema.SystemMessage(请把工单整理成不超过三行的摘要。),schema.UserMessage(ticket),})iferr!nil{return,err}ifmsgnil||strings.TrimSpace(msg.Content){return,errors.New(empty model output)}returnstrings.TrimSpace(msg.Content),nil}// ---- 模拟模型替代真实调用使测试无需网络与密钥 ----typestubModelstruct{replystringerrerror}func(m*stubModel)Generate(_context.Context,_[]*schema.Message,_...model.Option)(*schema.Message,error){ifm.err!nil{returnnil,m.err}returnschema.AssistantMessage(m.reply,nil),nil}func(m*stubModel)Stream(ctx context.Context,in[]*schema.Message,opts...model.Option)(*schema.StreamReader[*schema.Message],error){msg,err:m.Generate(ctx,in,opts...)iferr!nil{returnnil,err}sr,sw:schema.Pipe[*schema.Message](1)gofunc(){defersw.Close();sw.Send(msg,nil)}()returnsr,nil}// ---- 第一类测试确定性代码逻辑 ----funcTestSummarize_RejectsEmptyInput(t*testing.T){s:Summarizer{Model:stubModel{reply:不该被调用}}if_,err:s.Summarize(context.Background(), );errnil{t.Fatal(空输入应当报错)}}funcTestSummarize_PropagatesModelError(t*testing.T){sentinel:errors.New(model down)s:Summarizer{Model:stubModel{err:sentinel}}_,err:s.Summarize(context.Background(),打印机离线)if!errors.Is(err,sentinel){t.Fatalf(应当传播原始错误got %v,err)}}funcTestSummarize_RejectsEmptyModelOutput(t*testing.T){s:Summarizer{Model:stubModel{reply: }}if_,err:s.Summarize(context.Background(),打印机离线);errnil{t.Fatal(模型返回空内容时应当报错)}}funcTestSummarize_HappyPath(t*testing.T){s:Summarizer{Model:stubModel{reply:三楼打印机离线已影响合同打印。}}out,err:s.Summarize(context.Background(),打印机离线)iferr!nil{t.Fatal(err)}ifout{t.Fatal(摘要不应为空)}}// ---- 第二类测试效果评测需要判断不能只断言相等 ----typeevalCasestruct{namestringticketstringstubstringrequires[]string// 必须出现的关键要素forbids[]string// 不得出现的内容如编造根因}funcTestSummarize_QualityChecklist(t*testing.T){cases:[]evalCase{{name:保留关键事实,ticket:三楼打印机离线影响合同打印,stub:三楼打印机离线影响合同打印。,requires:[]string{打印机,离线,合同},forbids:[]string{驱动损坏,硬件故障},// 不得编造根因},{name:信息缺失应标注,ticket:打印机有问题,stub:打印机存在问题具体现象与位置未提供。,requires:[]string{未提供},forbids:[]string{确认是},},}for_,c:rangecases{t.Run(c.name,func(t*testing.T){s:Summarizer{Model:stubModel{reply:c.stub}}out,err:s.Summarize(context.Background(),c.ticket)iferr!nil{t.Fatal(err)}for_,want:rangec.requires{if!strings.Contains(out,want){t.Errorf(缺少要素 %q输出%q,want,out)}}for_,bad:rangec.forbids{ifstrings.Contains(out,bad){t.Errorf(出现了不该有的内容 %q输出%q,bad,out)}}})}}实机运行结果 RUN TestSummarize_RejectsEmptyInput --- PASS: TestSummarize_RejectsEmptyInput (0.00s) RUN TestSummarize_PropagatesModelError --- PASS: TestSummarize_PropagatesModelError (0.00s) RUN TestSummarize_RejectsEmptyModelOutput --- PASS: TestSummarize_RejectsEmptyModelOutput (0.00s) RUN TestSummarize_HappyPath --- PASS: TestSummarize_HappyPath (0.00s) RUN TestSummarize_QualityChecklist RUN TestSummarize_QualityChecklist/保留关键事实 RUN TestSummarize_QualityChecklist/信息缺失应标注 --- PASS: TestSummarize_QualityChecklist (0.00s) --- PASS: TestSummarize_QualityChecklist/保留关键事实 (0.00s) --- PASS: TestSummarize_QualityChecklist/信息缺失应标注 (0.00s) PASS ok example.com/einobook/art31 0.637s注意测试全绿但不代表应用“效果好”。这里验证的是逻辑正确性与清单符合度——真实效果必须在真实模型上跑评测集才知道。这个区分很重要测试通过 ≠ 质量达标。五、该测什么、不该测什么应该测原因输入校验与边界确定性问题最容易出 bug错误传播与分类决定线上能否定位问题状态与并发编排、幂等、会话隔离权限拒绝路径安全逻辑必须验证第 30 篇降级与超时失败路径容易被忽略第 29 篇检索召回RAG 的基础质量第 15 篇拒答正确性防止编造不该依赖测试断言原因模型输出的具体措辞每次都可能不同“输出一定包含某句话”措辞会变应检查要素而非原句端到端延迟的精确值受网络与负载影响应设阈值区间核心原则测“约束”和“要素”不测“措辞”。六、回归改动前后都要跑AI 应用最大的风险是改一处、坏一处改了提示词摘要变好了检索问答却开始编造。换了模型效果好一点但工具调用经常失败。调了切分粒度召回率上升但答案开始跑题。没有固定评测集你无法察觉这些退化。实践建议每次改动都跑同一套评测集。记录基线把当前指标存下来作为对比基准。覆盖负向用例无资料、越权、超时、空输入。特别关注拒答宁可它说“不知道”也不要它编。七、成本与效果一起看AI 应用的质量不能只看效果还要看代价指标说明正确性关键要素是否齐全、有无编造拒答正确率无依据时是否明确说明引用正确率标注的来源是否支持结论延迟首字节时间与总时长分别统计调用次数多轮 Agent 的实际模型调用次数费用单次请求的 Token 成本失败率各节点的失败比例第 28 篇一个准确但每次要 30 秒、花 5 毛钱的回答未必比一个 3 秒但略有瑕疵的回答更有价值。指标要一起看由业务决定取舍。第 28 篇的节点耗时统计、第 25 篇的MaxIterations、第 29 篇的重试次数都是这些指标的来源。八、常见问题输出不固定没法写断言把断言从“句子相等”改成“要素包含/不包含”。这是本篇示例的做法。测试要调真实模型太慢太贵用接口注入模拟实现测逻辑真实模型只用于少量效果评测。两层分开跑。改了提示词怎么知道有没有变差建评测集并记录基线改动前后对比。没有基线就只能凭印象。怎么测拒答准备一批资料里没有的问题检查系统是否明确说明“无依据”而不是编造。评测集要多大几十条覆盖典型场景即可起步。关键是持续跑而不是规模大。怎么测并发与会话隔离并发请求多个会话验证历史不串、状态不共享。这是最容易出线上事故的地方。十、小结AI 应用有两类验证确定性代码用单元测试模型效果用评测集不要混为一谈。依赖接口而非具体实现用模拟模型让逻辑测试可复现、免网络、零成本。断言“要素”requires/forbids不要断言“措辞”。必须有负向用例无资料、越权、超时、空输入重点验证拒答。效果与成本一起看延迟、调用次数、费用、失败率。测试通过 ≠ 质量达标真实效果必须在真实模型上评测。建立基线并持续回归否则无法发现退化。下一篇到这里生产实践的各个单点都已经讲过了如何对外提供接口第 27 篇、如何观测第 28 篇、如何保证可靠第 29 篇、如何守住安全边界第 30 篇、如何验证质量本篇。下一篇是全书最后一篇——《32综合实战交付一个企业知识与工单助手》。我们会把这 31 篇的能力拼装成一个完整可运行的应用路由、知识问答、工单查询、鉴权与拒答在同一个图里各就各位作为全书的收束。参考资料与版本说明资料核对日期为2026 年 10 月 9 日Eino 快速开始ChatWithEino QuickstartChatModel 使用说明Eino 官方示例仓库Eino 主仓库本文示例按Eino v0.9.21编写是一份可直接执行的 Go 测试文件已实机运行go test6 个用例全部通过正文输出为真实测试结果。
返回列表