WRITING
把存量代码和一柜子文档,做成真能干活的领域 agent:六步正确路径

我把一个垂直领域(水利)里 9 个跑了多年的存量算法,和一批只有 docx / pdf / xlsx 的领域文档,做成了一个每天真在用的问答 agent:问它一句「某某文号里这个指标怎么分档」,它自己去 grep、定位、整篇读原文,几秒到几十秒给出带出处的答案;问它一道要跑模型的计算题,它调后面那 9 个算法。
这篇不是复盘,是教程:按你从零走一遍的顺序,一共六步,每步只讲三件事——做什么、怎么知道成了、坑在哪。所有数字都是实测,不是估的。
先给一颗定心丸和一句警告。定心丸:整条链上没有向量数据库、没有微调、没有训练——主检索路径最后只剩三个动作:grep、定位、整篇读;砍掉花哨的那层之后,效果反而上去了。警告:这条链上最贵的失败不是代码写错,而是验证器说了谎——同一道验收题我跑了四轮红灯,其中三轮错的是判分的门,不是答案。
做 agent 的难点不在接大模型,在于每一层验证都可能假绿——门、评测、部署脚本、甚至「上传成功」四个字,全都要再问一句:你凭什么绿?
一、第 0 步:先把目标排成序,不然后面每一步都拧巴
做什么:动手前回答一个问题:这东西给谁用?我一开始写的是「双重目标:日常真用 + 求职展示」,两个并列。并列是错的,改成了排序:
| 优先级 | 目标 | 判据 |
|---|---|---|
| ① 唯一地基 | 工作上真有效 | 这周用它干成了一件本来要手工干的活 |
| ② 副产品 | 面试/展示 | 演示的是真实使用记录,不是专门做的 demo |
| ③ 排最后 | 可推广 | 等①跑顺再说 |
为什么这么排:自己每天用的东西,展示时讲的是真实使用记录,最有说服力;反过来,为演示设计一个自己不用的东西,一问细节就露怯。而「推广」意味着别人的机器上要有语料、要装、要维护,那是产品化成本,不该在第一行代码之前付。
排序落地成一条铁规矩:所有架构决策只按「我自己每天用它干活」来定。后面每一步的取舍(砍不砍向量层、修不修坏算法、要不要做多租户)全部用这一条裁决,不用开会。

怎么知道成了:任何一个「要不要做 X」的争论,你都能在十秒内用那句判据裁掉。裁不掉,说明目标还没排清。
坑:目标并列的真实代价不是抽象的「拧巴」,是具体的返工——我曾经为「演示效果」考虑过专门的假数据档,后来全部废弃:演示档和真实档一旦分叉,就得维护两份行为,而面试官恰恰最容易问到两者的缝。
二、第 1 步:盘家底——存量代码只挂牌,不修
做什么:9 个存量算法不是都能用的。逐个真跑一遍后,把每个封装成工具(tool)注册进一张表,并给每个工具挂一块四色牌:
| 牌 | 含义 | 例子(形态) |
|---|---|---|
| green | 真跑通过,量纲确认 | 评价类算法,输入输出闭环验证过 |
| degraded | 能跑,但有一件事没确认(比如返回值的单位量纲存疑) | 计算结果可能差一个 10⁴ 倍数,确认前强制标注「量纲未验证」 |
| blocked | 被外部条件卡住(比如缺一把第三方 API key) | 地理编码类,key 一到就能活 |
| broken | 数据契约或代码本身坏了 | 依赖的函数在整个仓库里 0 处定义 |
关键动作有三个:
- broken 的不修。这个项目的目标是 agent 能干活,不是给多年前的代码还债。修坏算法是无底洞,挂牌是一小时。
- 每块牌必须带证据:复现命令、观察到的真实输出、修复路径,三样齐才准挂 broken。「我觉得它坏了」不算。
- 让 agent 看得见牌子。对话里用户问到坏掉的算法,模型看到挂牌后主动说「这个当前不可用,原因是——」,而不是硬跑一遍然后崩给用户看。
最后盘出来是 10 组 22 个工具(文档检索类工具和 9 个算法平级注册):green 12、degraded 4、blocked 3、broken 3。超过四分之一的家底是有问题的——先承认,再开工。

怎么知道成了:调一次工具清单接口,逐条数目和牌色与你亲手跑出来的一致;然后故意问 agent 一道会落到 broken 算法上的题,看它是拒答并给出原因,还是硬跑。后者才是挂牌真正生效的证据。
坑:挂牌表千万别手抄进文档。我把这张表抄进项目说明的当天,它就和真实状态漂移了 7 处(实测)。正确做法是表由注册代码现算派生,文档里那份带一个「手改无效」的校验——校验红了说明有人手改了。凡是「同一个事实要在两个地方出现」的东西,都得这么办。
三、第 2 步:检索裁决——agentic search 取代 chunk RAG
做什么:领域文档怎么让模型读到?教科书答案是 RAG:把文档切成小块(chunk)、算向量、按相似度取 top-k 块塞给模型。我先走了这条路,然后用一类真实问题把它证伪了,换成 agentic search:让模型像人一样,先 grep / 按结构化线索定位到文档,然后整篇读,不够就换个词再来。
证伪的关键实验是「对比类」问题——「新旧两版标准有什么区别」这种,恰是专业工作里最常问的题型之一。在 chunk 检索下它无解,而且是结构性无解:
- 这是容量问题,不是排序问题。两版标准合起来,top-k 最多分到 4 个 chunk,约等于正文的 3.8%。换更好的向量模型、加重排序(rerank),都只是在换「哪 3.8%」,救不了「只有 3.8%」。
- 更阴的一层:相似度检索系统性地优先召回两份文档措辞相同的部分——而「区别」恰恰住在措辞不同的地方。检索范式和这个问题类型是语义相反的。
- 反过来看判别力:这类语料里最能锁定一篇文档的短串——文号、地名、指标名——恰恰是向量嵌入最不擅长保留的信息。而 grep 对它们是精确命中。

所以主路径定为三个动作,chunk 检索降级为兜底(只在只有模糊描述时用):
| 动作 | 什么时候用 |
|---|---|
| grep | 知道正文会出现哪个串(文号 / 指标名 / 专名) |
| 定位 | 有结构化线索:文号、年份、地区、文档类型(从文档确定性抽取,零 LLM) |
| 整篇读 | 已锁定文档,分页读全文——答案的每个数字都出自原文 |
| 兜底检索 | 只有模糊描述时。牢记:片段里没有 ≠ 文档里没有 |
两个省钱的细节:① 向量层「先关后删」——先确认线上依赖清单里根本不含向量相关的包(并写一条测试断言它永远不含),砍层对线上是零改动,风险比想象小一个量级;② office 格式(docx/pdf)是二进制,grep 源文件永远搜不到中文,得先抽正文进索引,grep 打在抽出来的正文上——这一条不做,「搜过、没有」就是句谎话。
怎么知道成了:拿那道对比题当验收:chunk 检索下答不出的「逐维度差异」,换成定位 + 两篇整读之后,agent 给出两版标准的逐维度对照表,每格都能在原文找到出处。这道题从「结构性无解」变成「秒级全对」,就是裁决正确的证据。
坑:别把「Claude Code 都放弃了 RAG」当口号抄。它成立有个前提——文件就在本机盘上,grep 得到。这个前提在我的场景刚好也成立(语料全在本地);如果你的语料在别人的数据库里、或者大到枚举不动,这套裁决不直接迁移。抄结论之前先核对前提。
四、第 3 步:评测门先立——并且做好「门自己也有 bug」的准备
做什么:在优化任何东西之前,先立三层确定性评测,全部可以一条命令跑、红绿分明:
- L0 循环层:agent 循环的关键路径(该拒答的拒答、该重述的重述),用录制好的固定剧本回放,不打真模型;
- L1 工具层:22 个工具逐个真调,输出和黄金值(golden)对账,纯确定性,零 LLM;
- L2 检索层:给定问题,检索层必须命中该命中的文档;
- 端到端:几道真题打真模型,答案里必须出现的数字和必须成对出现的搭配,逐条硬判。
另外两条纪律:黄金值文件不许自己改自己封——改动必须由独立的第三方复核实跑确认后盖章;判分脚本对「空集」保持敌意——扫描根不存在、枚举为空,一律报红,拒绝在空集上报绿。
怎么知道成了:三层门加 e2e 全绿,而且你故意放一个已知 bug 回去,门真的变红。没做过这个反向实验的门,绿灯不算数——它可能从头到尾就没在看。
坑:本步最大的坑值得加粗——判分的门自己也有 bug,而且比答案的 bug 更隐蔽。同一道端到端验收题我跑了四轮红灯,事后拆帐:
- 第一轮:答案里的「-90/-80」是分档下界,被判分器当成负数捕获,误判缺数字;
- 第二轮:模型把原文的「60≤得分<80」改写成「60-79」——79 这个数原文没有,门正确地拒了,该修的是生成侧的规矩(「区间照原文形式写,禁改写」);
- 第三轮:上下文预算上限和最大步数打架,证据集齐了却被预算截断,答案第一句「结果不完整」;
- 第四轮:通过。

四轮里三轮,要修的是门和配置,不是答案。如果你把每次红灯都当成「答案错了」去改生成侧,会把本来对的东西改坏。红灯之后的第一动作是拆帐:这次红的到底是谁。
五、第 4 步:延迟治理——从 418 秒到 23 秒,先拆帐再动刀
做什么:功能对了之后,同一道验收题要 418 秒——不可用。治理分三刀,顺序很重要,第一刀不是优化,是拆帐:给生产 trace 逐段计时,看时间到底花在哪。拆出来的帐震撼:
| 段 | 耗时 | 是什么 |
|---|---|---|
| 前三步选工具 | 各 9–14 秒 | 每步一次模型调用,固定开销约 5 秒 |
| 第四步 | 106.2 秒 | 模型在循环里写了整篇答案,但循环只拿它当停止信号,正文全部丢弃 |
| 收尾综述 | 50.4 秒 | 再从头生成一遍成稿(用户看到的是这份) |
| 所有工具调用合计 | 0.3 秒 | grep / 定位 / 整读,几乎免费 |
六成的时间花在「生成了又扔掉」上,而所有真正干活的工具加起来不到半秒。 不拆帐直接上缓存、换模型、调并发,全是瞎调。

三刀依次是:
- 流式 + 收尾契约:循环步里禁止写成稿(写了也是扔),成稿只在收尾生成一次,并且边生成边流式推给前端——用户从「空屏干等 418 秒」变成「第一个字先到,正文哗哗出」。418 → 249 秒,前端首字从「全程空屏」变成流式 44 条增量。
- 定位即直读:起手先做一次零 LLM 的确定性侦察(按文号/年份/关键词列 12 条候选文档);如果问题里的文号在文件名级精确命中,跳过整个多步循环,直接整读作答。文号类问题从 72–126 秒降到 21.5 秒、一次模型调用。
- 换更快的模型供应商:前两刀把结构成本砍掉后,剩下的墙钟基本是模型生成速度本身,这时换供应商才有意义(先换只是掩盖结构问题)。同题 122.9 → 32.5 秒,浏览器里同路径实测 21.5 秒、首字 14.4 秒。
怎么知道成了:不是看平均值,是同一道题、同一套判分门下的前后对比:418.3 → 248.9 → 122.9 → 32.5 → 最终 23.1 秒,每一步都有 trace 留档;且首字时间单独看(流式的意义就是首字,总时长骗不了等待体感)。
坑:优化的顺序错了会互相掩盖。如果我第一刀就换快模型,「整篇生成又丢弃」的结构病会被快模型的速度盖住,418 秒变成大约 80 秒——看起来赢了,实际上六成浪费还在,而且再也没人会去找它。先拆帐,先砍结构浪费,最后才换硬件/模型。
六、第 5 步:全仓找修与上线——以及整条链的验收清单
做什么:上线前做一轮系统性的找茬,方法比工具重要,形态是四段:
- 分区找:把仓库切成互不重叠的七个分区(agent 循环 / 接口层 / 检索 / 模型层 / 工具层 / 前端 / 运维脚本),每区独立找 bug 和可优化点;
- 对抗核验:每条发现派一个「默认证伪」的核验者亲手复现——立场就是反驳它,复现不出来就否决。第一轮 21 条全被判真、零否决,零否决本身可疑,所以修每条时又按「改前红 / 改后绿」的反向用例再验了一遍;
- 修必配反向测试:每条修复配一条「把 bug 放回去就变红」的用例——没有反向测试的修复不算修好;
- 改完复审:对全部改动的 diff 再来一轮对抗复审。这轮共 21 条新发现,4 条被核验者反驳掉,3 条高危属实——其中最险的一条:部署脚本写下的运行配置,服务端根本不读,而部署后的第 12 步验证读的恰好是脚本写的那份——十二步全绿,线上聊天必死。典型的「验证路径 ≠ 执行路径」假绿。

两轮合计 42 条发现,全部闭环(修掉、或记录不修理由)。然后上线,部署脚本本身就是一条 12 步的验收链:依赖对账、密钥探活、索引条数、拒答形态、一个算法线上实跑对黄金值、边缘网络可达——任何一步红即中止。
怎么知道成了:部署脚本的十二步绿不算完——最后一步永远是以真实用户身份,在线上问一道有标准答案的真题。我上线后的验收是:同一道文号题,线上实跑,逐维度分值全对、引文 5 条、秒级返回。这才是「上线成功」。
坑:两个,都是「对账工具对某类错误天生失明」:① 文件级 sha 对账能抓「文件内容漂了」,但对「整个包漏收」是瞎的——漏掉的文件不在清单里,对账永远绿;解法是在对账之后加一发 import 冒烟(真加载一次入口模块)。② 「上传/同步成功」只证明字节发出去了,不证明对端收下且能跑;所以部署验证必须打到对端的运行时(线上实跑一个算法、真答一道题),不能停在传输层的回执上。
最后,把整条链的「别信什么/去看什么」收成一张表。这张表就是本文的验收清单:
| 步 | 别信什么 | 去看什么 |
|---|---|---|
| 0 目标排序 | 「两个目标都要」的并列 | 每个取舍能否被「我自己每天用」一句裁掉 |
| 1 盘家底 | 手抄的状态表、「我觉得它能跑」 | 从注册代码现算的牌 + 每块 broken 牌的复现命令 |
| 2 检索裁决 | 「换个更好的向量模型就行」 | 对比题的逐维度对照表,每格有原文出处 |
| 3 评测门 | 门的绿灯 | 故意放回一个已知 bug,门真的变红 |
| 4 延迟 | 平均耗时、换模型后的总时长 | 逐段拆帐的 trace + 同题同门的前后对比 + 首字时间 |
| 5 上线 | 部署脚本十二步全绿、sha 对账、「同步成功」 | 以真实用户身份在线上答一道有标准答案的真题 |
回头看,这个项目最值钱的经验不是任何一项技术选型,而是一个贯穿六步的动作:每当一个东西报绿,先问它的枚举源是谁、它的验证路径和执行路径是不是同一条。 门会错、对账会瞎、回执会假——把「验证器本身」当成头号嫌疑人来审,剩下的都是体力活。做到这一点,接大模型真的就是整条链上最省事的部分。
相关文章
订阅
新文章都先发在这里。用 RSS 订阅: /feed.xml