← 返回博客
架构2026-08-26 11:02:048 分钟 · 2,294 0

给 Agent 出一张考卷:评测驱动开发实录,从 12 分到满分

我给博客 AI 助手建了一套 20 题金标准评测集。第一次跑分 12/20——每一个丢分的题目背后,都藏着一个真实缺陷:知识盲区、未查先拒、门控误伤、中文匹配漏配。这是一份带完整解题过程的错题本。

上一篇写完的当晚,我给 AI 助手跑了一次"考试"。

考卷是 20 道金标准问题,覆盖文章事实、个人项目、身份档案、拒答边界四个象限。跑分器逐题调接口、断言回答,最后吐出一行结果:

通过 12/20 | 平均延迟 6768ms

12 分。这个数字比任何主观感受都有冲击力——我一直以为它已经"产品级"了,但考卷说:你有 8 个洞。

这篇文章记录这 8 个洞是怎么一个个填上的。不是功能介绍,是一份带解题过程的错题本。

一、先出考卷:断言维度设计

评测的前提是"答案可判定"。聊天式输出千变万化,不能比对全文,只能断言结构性质。我给每道题定义了六种断言维度:

{
  "q": "象棋项目用什么算法?",
  "expect": {
    "must_contain": ["Alpha"],
    "must_cite_any": ["/resume"],
    "tools_any": ["get_project", "list_projects"]
  }
}
  • must_contain:回答必须出现的关键词(Alpha-Beta 剪枝)
  • must_reject / allow_reject:是否必须/允许拒答(天气题必须拒,写诗题随缘)
  • no_sources:不应附带来源(拒答时带来源说明检索误召回了)
  • must_cite_any:来源应命中预期 URL 之一
  • tools_any:工具轨迹应出现预期工具——验证的是路由决策,不只看答案

最后一项是关键。答案对不代表过程对:如果模型靠 system prompt 里硬编码的身份信息蒙对了"博主是谁",却没调用档案工具,那换个问题它就会露馅。断言工具轨迹等于把"推理路径"也纳入了考试范围。

配套还有两个工程细节:评测请求带 admin token 走专用通道,旁路 IP 限流和缓存(否则 20 题连跑会被自家风控掐死,缓存命中还会让考试失真);日志里这类请求标记 ip=eval,统计时可剔除。

二、12 → 14:盲区与边界

第一批修复对应两类失败。

知识盲区:「临床试验系统用了什么技术栈」「博主会 Go 吗」全部拒答。排查发现这些信息明明在简历文本里,但简历切块后的向量表征被稀释,语义检索够不着。修法不是调阈值——项目名、技能栈这类有限枚举的信息根本不该依赖概率召回。我把档案拆成结构化 JSON,新增 get_profile() 工具精确查询,与已有的 get_project() 同一设计哲学。

域外边界:「北京天气怎么样」不但没拒答,还煞有介事地附了一篇文章当来源;而「帮我写首诗」模型倒是很想帮忙。这说明拒答规则的定义太窄——只覆盖了"知识域内无答案",没覆盖"请求本身在域外"。Prompt 补上一条:与博客无关的请求(天气、创作、生活问询)一律用固定句式拒答。

14 分。但真正的硬仗在后面。

三、Prompt 劝不动的,让架构来劝

第三轮失败暴露了一个更顽固的行为:未查先拒。「点赞功能怎么实现的」这种题,模型一轮工具都没调,直接回答"暂无相关内容"。我在 Prompt 里写了"严禁未查先拒"——没用,下一轮照样拒。

这就是 Prompt 工程的天花板:它能引导概率,不能保证行为。模型对某些问题的"直觉性放弃"稳定存在,措辞再严厉也只是概率意义上的改善。

解法在架构层。观察到一个可利用的性质:拒答句是固定的九个字——「博客中暂无相关内容」。而我们的回答走流式透传,那么:

第零轮流式输出时,先缓冲一个九字符的窗口不透传。窗口攒满后判断:开头若恰好是拒答句 → 整句拦截(此时还没向客户端推过任何字节),注入一条强制检索指令进入下一轮;否则清空缓冲照常透传——真回答的首字延迟几乎不受影响。

if holdFirst && !emitted && full.Len() < windowLen {
    full.WriteString(piece)          // 窗口内静默缓冲
    if strings.Contains(full.String(), rejectPhrase) {
        return streamRound{rejectHold: true}   // 无损拦截
    }
    // 不是拒答 → 冲出窗口,开始正常透传
}

被拦截的问答会追加两条消息继续循环:assistant 的拒答句 + 一句"请立即调用检索工具核实,确认没有才允许拒答"。同时用标志位保证强制检索最多触发一次,防止死循环。前端还会收到一条状态事件——访客看到的是「🔍 二次确认检索中…」,黑盒变成了可见的严谨。

这一轮之后 18 分。这个案例我总结成一句话:Prompt 管意图,架构管行为。当一个行为规则足够重要、重要到不容许概率性违反时,把它从提示词挪进代码里。

四、18 → 19:一个数字破案

剩两道顽固错题,「点赞功能怎么实现的」重试三次都是拒答。这次仲裁机制明明工作了——工具轨迹显示模型被拦截后真的去调了全文搜索,然后……查了个寂寞,依然拒答。

顺着工具的返回往下挖,真相在一个数字上:

MATCH(title, content) AGAINST ('点赞') = 4.69
门控条件: HAVING score >= 6

那篇讲点赞的文章同时讲了分页、JSON-LD、搜索收录四个主题——单主题词频被整篇稀释,4.69 分死在我自己设的噪声门控下。当初为了挡住低相关噪音加的这个阈值,此刻正把真命中挡在门外。每一道防线都会误伤,关键是知道它的弹道

修法没有动阈值(降门控会放回当年过滤掉的噪声),而是在工具内部做了自愈:全文搜索未命中时,自动降级补一发语义检索,两路结果合并返回。全文通道擅长专有名词精确匹配,语义通道擅长含义泛化——空结果不再意味着"没有",而是"该换条路了"。工具的空结果是信号,不是终点。

19 分。最后一题是纯粹的中文语言问题:用户问「临床试验系统」,知识库里叫「临床试验数据管理系统」——中间插了四个字,子串匹配直接漏配。解法是把匹配从 Contains 升级为 2-gram 重叠率:短查询切成相邻二字片段,统计被长名称覆盖的比例,超过 40% 即命中。"临床/试验/验系/系统"四个片段命中三个,75%——稳稳过线。

20/20。

五、彩蛋:评测连我也抓了三次

这套评测上线过程中还抓到三个 bug,肇事者是我自己:

  1. 一个补丁脚本链式执行时中途报错,&& 短路导致后半段修改静默丢失——部署后行为不对才发现;
  2. Go 字符串拼接忘了加 +,中文标点直接进了语法层,编译器报了满屏 invalid character;
  3. 同一段按钮逻辑写在两个补丁脚本里各执行了一遍,线上出现了两个一模一样的按钮,其中一个调用的函数从未被声明。

第三次尤其讽刺:那个死按钮安静地挂在生产环境好几天,没有任何报警——因为它"看起来完全正常"。**人眼 review 对这类残留几乎免疫,能抓住它们的只有两种东西:类型检查器和自动化断言。**这也是我对评测集价值最切身的体感:它不光替我看着 AI,还顺带看着我。

六、账本

阶段分数修复
基线12
第一轮14档案工具 + 域外拒答边界
第二轮18拒答仲裁(架构级)
第三轮19双通道自愈 + 门控复盘
第四轮20中文 2-gram 匹配

总投入约两天,其中真正写"新功能"的时间不到三分之一——剩下都在定位问题、校准考卷、以及被自己的工程债绊倒。

这四篇系列至此收尾:《装上一个了解我的 AI》解决有无,《Agentic RAG》解决能力,《Demo 与产品的最后一公里》解决体验,这一篇解决确定性——让"它大概没问题"变成"这 20 道题它每次都对"。

AI 应用的开发范式正在从"调 Prompt 看感觉"转向"改一行,跑一遍考卷"。早一天建立评测,就早一天从玄学里毕业。


系列完整目录: ① 装上一个了解我的 AI · ② 从 RAG 到 Agentic RAG · ③ Demo 与产品之间的最后一公里 · ④ 本篇

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/153