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 个用例全部通过正文输出为真实测试结果。
企业数字化 ERP 产品动态
相关推荐
Codex-X海鸥3.0破甲离线启用教程:4步配置完成,1句应答验证多域路由 Codex-X海鸥3.0破甲离线启用教程:4步配置完成,1句应答验证多域路由 【免费下载链接】Codex-X OpenAI Codex 桌面端/CLI 的可视化管理工具,具有Provider/API 切换、会话同步、提示词注入、Skills/MCP 管理、TOML 配置可视化的跨平台工具。 项… · 2026/11/11 1:11:34
告别手写前端:Infinite Monitor自然语言生成Widget的5大核心场景与上手教程 【免费下载链接】infinite-monitor Monitor anything in real time 项目地址: https://gitcode.com/gh_mirrors/in/infinite-monitor 点击查看 免费下载 Infinite Monitor 是一款 AI 驱动的仪表盘构建工具:用自然语言描述你想要的 Widget,AI… · 2026/11/14 10:28:50
芜湖电工证考试网址值不值得考?怕被中介坑?这样避坑 芜湖电工证考试网址值不值得考?怕被中介坑?这样避坑 你是不是正在为考电工证发愁?不知道从哪里报名,怕被中介骗钱?特别是现在网上一搜【芜湖电工证考试网址】,信息五花八门,真假难辨。电工证值不值得考?这确实是个现实问题,尤其对于想转行或者提升自… · 2026/11/15 15:34:53
学校没让考电工证,考前押题能帮你上岸吗?学历不高怕报不上名? 学校没让考电工证,考前押题能帮你上岸吗?学历不高怕报不上名? 你是不是也这样,学历不高,学校没让考电工证,想着以后能找个好工作,结果发现连报名资格都成了难题?别急,今天就带你理清楚这个事, 考前押题… · 2026/11/15 15:34:53
临颍电工证焊工证复审多少钱?实操考试心里没底怕挂科怎么破 临颍电工证焊工证复审多少钱?实操考试心里没底怕挂科怎么破 实操考试心里没底怕挂科?临颍电工证焊工证复审多少钱?这些问题你不是一个人在纠结。很多在鹤壁从事劳务班组的负责人,尤其是那些需要持证上岗的电工、焊工,对复审流程和费用心里没底,担心挂科… · 2026/11/15 15:34:53
什么地方可换电工证?官方报名入口在哪?别再被中介坑了 什么地方可换电工证?官方报名入口在哪?别再被中介坑了 你是不是也遇到过这种情况:听说要换电工证了,但不知道去哪报名,怕被中介骗钱?别急,这篇文章就是为你量身打造的,从 什么地方可换电工证 到 官方报名入口… · 2026/11/15 15:34:53
谢岗低压电工证复审内部消息:怕考不过白交培训费?看懂这些你就能稳过 谢岗低压电工证复审内部消息:怕考不过白交培训费?看懂这些你就能稳过 你是不是也担心,花了几千块报的谢岗低压电工证复审,最后还是没通过?别急,这不是你一个人的问题,很多人都在复审路上踩过坑。今天,咱们就来聊聊 谢岗低压电工证复审 的那些“… · 2026/11/15 15:34:53
电工证需要看哪些书籍才能上岗?电子证书怎么领 电工证需要看哪些书籍才能上岗?电子证书怎么领 急需特种作业证才能上岗找工作,别再等了。你可能刚从学校出来,或者想转行进入电力行业,但手头没有电工证,连面试资格都没有。电工证是从事电力作业的硬门槛,特别是在建筑、制造业、能源行业,没有这个证,… · 2026/11/15 15:33:53
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 1:51:20
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 4:02:19
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 3:02:05