先说结论:这个东西到现在还在线上跑,每个月要烧掉 40 多亿 token,代码量已经堆到了 20 万行,开发周期是九个月,开发人员,就我一个。
可能你觉得我是在做一个反向代理中转站,或者套壳对话产品,其实都不是。我做的是一个基于 Harness 架构的多智能体应用,核心是把一堆擅长不同任务的模型 Agent 挂在同一条主控回路上,让它们像一条流水线里的工人一样,互相配合,而不是各干各的。这套东西没有团队,没有外部资源,从架构设计到前端页面再到云上部署,全是我一个人扛下来的。
今天我把这段经历摊开来写一写,不是晒成绩,而是把我为什么选择 Harness 架构、40 亿 token 到底是怎么烧掉又怎么省下来的、一个人维护 20 万行代码的工程方法,一次性讲清楚。
1. 为什么是 Harness 架构:它不是编排,是“套马索”
1.1 Agent 一多,调用链就全乱了
一开始我并不是 Harness 思路,而是照着大家熟知的 DAG 编排去设计的。流程图画得漂漂亮亮:A Agent 抽取信息,B Agent 改写内容,C Agent 查漏补缺,D Agent 汇总输出。前三天写得很爽,到第四天就开始崩溃。
崩溃的原因特别朴素:任何一环只要出了非预期结果,后面所有环节全部作废。Agent 带着大模型天生的不确定性,没有一次输出是一样的。今天跑通了,明天同样的输入换了个措辞,链路就断在半路。更麻烦的是上下文。一段文本被多个 Agent 转手,转着转着就走样了。原来的关键信息可能被某个 Agent“好心”做了压缩,也可能被另一个 Agent 顺手改写,最后汇总出来的内容和原始输入差之千里。
那段时间我反复在做一件事:定位“到底是哪一步把数据搞坏了”。每次定位都要把中间结果全部 dump 出来,人工比对。这种模式在三个 Agent 以内还能忍,一旦超过五个,排查成本指数级上升。
我后来把整套东西推倒重来,换成了 Harness。
1.2 Harness 架构的取舍:主绳决定方向,零件只负责用力
你可以把 Harness 理解成骑马时套在马身上的那副绳具。重点不是单个马镫或马嚼子,而是那几根主绳怎么把所有力量汇到同一个方向。对应到应用里,就是几个核心部件:
第一,RunContext。每次任务都是一个 Run,RunContext 保存任务的主输入、当前阶段、中间产物和最终输出。任何 Agent 只能读写自己被授权的上下文切片,不能看到全局内存。这就解决了“上下文被污染”的问题。
第二,主控制循环。Harness 不是让 Agent 自由互喊,而是由主循环根据当前阶段和状态,决定下一步调用哪个 Agent,哪个工具,以及拿什么数据喂进去。Agent 在我这套架构里更像是“被装配到槽位上的执行器”,不是拥有对话主导权的实体。
第三,资源槽位。每个工具、每个 Agent 都挂载到命名的槽位上。主循环按名字调用槽位里的实现,路由规则可以随时改。这样换模型、换工具实现,都不需要动主流程代码。
第四,Token 预算。每个 Run 都有 token 预算,Harness 在请求发出前先算一笔账,如果预算不足,就降级模型、缩小上下文或者直接暂停。预算不是辅助功能,而是架构的一部分。
第五,审计轨迹。每个状态迁移都记录,每次模型调用都留 trace。出了任何问题,可以按时间线重放整个 Run。
我和常见编排方案的对比也很直接:
| 维度 | 传统 DAG 编排 | 通用 Agent 框架 | Harness 架构 |
|---|---|---|---|
| 核心坐标 | 流程步骤 | 智能体对话 | 可控执行回路 |
| 上下文管理 | 各节点各自处理 | 容易共享污染 | 按 RunContext 切片隔离 |
| 容错方式 | 节点重试 | 靠模型自我恢复 | 断点暂停 + 人工介入 |
| 适合场景 | 确定性流程 | 开放式对话 | 半确定性、高价值的生产链路 |
这套结构最大的好处是:复杂度收敛在一条主循环里。整个系统可能有很多 Agent、很多任务类型,但它们的行为都遵循同一个生命周期,出问题时不需要到处找原因。
1.3 为什么它适合一个人单干
一个人写 20 万行代码,最怕的不是代码量,而是认知负载。如果架构里到处是不同的调用约定、不同的状态管理方式,一个人根本记不住。
Harness 把复杂系统压缩成了“一个主循环 + 一堆槽位”。我只需要保证这条主循环是对的,往上加 Agent 就是加槽位,往外接新任务就是配新路由。九个月里我几乎每天都在往里面加功能,但核心 Runtime 的代码改动量很小。这就是 Harness 对单人开发最大的红利:加功能不会把主架构推倒。
2. 九个月的技术路径:20 万行代码是怎么长出来的
2.1 前三个月:先写一个不智能的骨架
前三个月基本没有智能可言。我首先写的是一个能跑、能暂停、能断点续传、能回放的 Run Runtime,而不是模型代码。
核心是一个状态机,任务的流转大约是这样:
created -> queued -> running -> paused -> completed | | +---- failed/cancelled每一个 Run 都带着 RunContext,里面有任务根输入、阶段标签、重试次数、当前上下文摘要。任何一步失败,Harness 会把现场冻结,然后进入 paused 状态,等待人工修复或者兜底策略。
我不急着接大模型,是因为我清楚:模型能力是会用错的,但状态机不会。只要主流程够稳,模型出错的时候我可以把它隔离开,而不是让错误顺着链路传导。
这几万行代码如果非要说有什么含金量,我觉得是“可暂停性”。Agent 任务跑到一半发现数据不对,Harness 允许我改完中间数据再继续跑,而不是从头再来。这个设计在后面省了我无数个加班夜。
2.2 第四到六个月:模型网关和 Token 中控
骨架搭好之后,我开始接入模型。写到这个时候我才意识到,直接调各家 API 的 SDK 是个陷阱。不同模型的消息格式、参数命名、限制逻辑都不一样,如果业务代码到处直接调 SDK,后面任何模型升级都是灾难。
所以我做了一个统一模型网关,所有请求都走同一套接口:
response = await gateway.chat( messages=messages, tools=available_tools, model_route="extract_cheap", # 路由名,而不是固定模型名 priority="normal", )网关内部负责模型路由、失败重试、请求合并、流式转发、自动降级。业务层永远不关心现在到底是哪个模型在处理任务,只关心“用哪个路由”。
同一时间,我上了 Token 中控。每个请求都要经过 TokenMeter,它做三件事:记录每个 Run 的 token 消耗、检查预算、决定是否允许继续。刚开始觉得很繁琐,后面发现没有了这个东西,系统根本没法控制成本。
这个阶段的代码量涨得很快,模型网关加 TokenMeter,再加上各种模型适配器,差不多写到 5 万行。
2.3 第七到九个月:产品化、插件化、跑量
从第七个月开始,我不再只折腾核心 Runtime,而是做产品化。插件 SDK、外部 Webhook、可视化面板、运营后台,一个都不能少。
插件 SDK 是我想了很久的产物。我希望外部的人能注册一个“工具处理器”,不需要理解主循环内部细节,只要实现一个接口:
from harness import BaseHandler class CustomExtractor(BaseHandler): async def run(self, context, payload): # 处理上下文里的任务数据 return result插件框架一出来,系统复杂度又从 Runtime 转移到了插件层。好处是很多场景化的能力可以独立开发、独立测试,坏处是插件数量一多,我的测试保障压力也上来了。
到第九个月,代码量终于跨过了 20 万行。这一个月的节奏很单调:写脚本压测、修 bug、写文档、再压测。
2.4 20 万行代码的结构地图
我大概分了这么几块:
| 目录 | 作用 | 代码量(行) |
|---|---|---|
| harness/runtime | 状态机、RunContext、生命周期 | 1.8 万 |
| harness/gateway | 模型网关、路由、适配器 | 1.2 万 |
| harness/token_meter | 预算、计量、限流 | 0.8 万 |
| runners/* | 各类主流程 Runner | 3.0 万 |
| plugins/* | 插件和工具实现 | 5.0 万 |
| web/* | 可视化面板、运营后台 | 4.0 万 |
| ops/* | 部署、监控、日志 | 1.2 万 |
| tests/* | 测试套件和夹具 | 3.0 万 |
加起来 20 万行左右。这其中有相当一部分是插件层和前端,真正的核心 Runtime 其实很小。这种“小核心、大插件”的结构,是我一个人能撑下来九个月的关键。
3. 40 亿 Token 是怎么烧出来的,又是怎么省下来的
3.1 先算账:钱到底花在哪了
40 亿 token 不是虚数,是我看了账单后倒推回来的。
我给自己算了一笔账:一个常见的文档清洗流程,从原始文档进来,到结构化数据入库,平均单条任务要消耗 12 万 token。每天跑大约 1100 条任务,日消耗就是:
12 万 token × 1100 条 = 1.32 亿 token/天 1.32 亿 × 30 天 = 39.6 亿 token/月一个任务里包含了很多步骤:分块、要素抽取、格式统一、规则校验、矛盾修正、二次抽取、最终输出。每一步都是一次模型调用,每一次模型调用都要带上已经抽取出来的前序结果,token 自然越滚越大。
刚开始我也不觉得有什么,直到第一个月的账单出来,我才意识到如果不做成本控制,我一个人赚的还不够付模型费用的零头。
3.2 Token 中控的三板斧:缓存、路由、压缩
第一板斧是 Prompt 缓存。系统 prompt、few-shot 示例这些几乎不变的前缀,用缓存能力能省下大量重复计算的 token。实际跑下来,系统 prompt 缓存命中率常年保持在六成以上,这部分直接省掉了重复输前缀的开销。
第二板斧是语义缓存。对于重复性很高的任务,不是每次都重新跑大模型,而是先用小模型做向量化,然后到缓存里找相似结果。如果一段文本和某个历史任务在语义上高度相似,直接复用前序结果,只做轻量校验。刚开始我不敢相信缓存结果,后来加了“置信度阈值”,低于阈值的自动转人工重跑,整体准确率没有明显下滑。
第三板斧是模型路由。不是所有任务都需要最强模型。我在路由里分了几档:信息抽取、简单分类用便宜档;逻辑推理、规则矛盾修正用旗舰档。两者比例大概七比三,token 成本直接降了一个量级。
除此之外,我还做了上下文压缩。长流程中不断累积的中间产物必须做滑窗式截断和阶段性摘要。举个例子,一个任务跑到第 8 步,前面 7 步的所有原始输出不可能全带进第 8 步。我会用一个小模型把前 7 步压缩成 2000 token 的摘要,再喂给后面的模型。这样单任务的平均上下文长度从 3 万 token 降到大约 1 万 token。
3.3 有些 token 不能省,省了就是给自己挖坑
虽然我很抠,但有几个环节我一直坚持用最强模型,而且不限制它的思考深度。
第一个是“矛盾检测”。文档里的多个字段如果互相冲突,小模型经常直接猜一个答案,大模型则会发现冲突并触发复核流程。这个环节省 token 省出来的,是大量返工成本。
第二个是“最终输出前的校验提示”。在最后一步,Harness 会把完整结构丢给一个强模型,让它做一次挑错。这一步单次消耗很高,但它能拦住一半以上的低级错误。很多时候模型在单步里都正常,但组合起来就是有问题,这个“整体审视”的步骤,小模型做不到。
我的原则是:重复劳动用便宜模型,关键判断用贵模型。有些 token 看起来贵,但比错数据的返工成本便宜太多了。
4. 一个人维护 20 万行代码,靠什么不崩
4.1 工程纪律:把自己当成一个团队
一个人写 20 万行代码,最大的风险不是写不出来,而是隔一个月自己都看不懂自己写的东西。
我的办法很土:每个 commit 都绑定 issue。哪怕是一个很小的改动,我也会先写一句任务描述,再动手。比如“修复 extract_blocks 在表格结构下的重试死循环”,比“fix bug”这样的提交信息有用一百倍。
另外,所有关键设计我都写进了 docs 目录,不是那种冗长的设计文档,而是“决策记录”:当时为什么这么选,放弃了什么,代价是什么。九个月后回看,这些决策记录比代码本身更值钱。
4.2 没有 QA,就靠自动化测试兜底
我不可能像团队那样配置一堆测试岗位,所以我的策略是把测试做成“安全网”。
首先是黄金测试集。我整理了一批真实场景的数据,每一个都标注了“正确输出”。每次大改动之后,自动跑一遍黄金集,看输出偏离了多少。这里要对“偏离”做语义匹配,不是逐字比较,而是看关键字段是否一致。
其次是成本回归测试。每次改完路由和缓存逻辑,我会用同样的任务集跑一遍,比较 token 消耗。如果某次改动后 token 涨了 20%,就算功能没坏,我也不会合入,因为这种成本膨胀一旦放量就是灾难。
还有一个技巧是日志追回。Harness 的审计轨迹保留每一次调用的上下文,所以出问题的时候,我不会只用“现在还能不能复现”来判断,而是直接重放那一次的调用链,看模型到底接了哪些输入、输出了什么。这种重放能力让我即便是一个人,也能像有整个技术支持团队一样快速定位问题。
4.3 我踩过的几个典型坑
第一个坑是共享全局内存。早期为了省事,我把所有 Agent 的中间结果放在一个全局字典里,这确实方便,但也埋了雷。两个任务并行执行时,A 任务写入的字段会被 B 任务读走。排查了一个通宵,最后把全局字典全部改成 RunContext 隔离,并给每个上下文加了 owner 校验。
第二个坑是重试无幂等。模型网关超时重试时,没带任务 ID,导致同一个任务被处理了两次。第一次输出已经写入数据库,第二次又把同样的内容追加了一遍。后来所有写入操作都加了幂等键,状态机才能安全重放。
第三个坑是模型升级后行为漂移。我用同一个测试集跑了十几次,发现某次升级后,输出格式偶尔不一致,黄金测试看不出来,单独跑也正常,但放到批量任务里就偶发异常。最终解决方案是锁定模型版本,新版本先在影子环境跑一周,再切流量。
5. 实战排查:Token 用量和任务异常速查
5.1 某天早上发现 token 用量飙了 3 倍
这种事情我遇到过好几次。第一次看到的时候差点冲动删库,后来发现 90% 的用量飙升都是同一个原因:缓存失效。
排查不长,按顺序走就行:
第一步,看路由日志。先确认是不是某一个路由的请求量涨了。如果总请求量没变,但 token 涨了,那就是单请求的输入长度变大了,问题出在上下文累积。
第二步,看缓存命中率。如果命中率突然从 60% 掉到 10%,基本可以确定是 prompt 前缀变了。比如系统提示里加了一个时间戳,导致所有缓存 key 全部失效,这是最常见的低级错误。
第三步,看单任务平均输入 token 分布。找到那些拖后腿的任务,点开审计轨迹,看看是哪一步把整个历史记录塞进去了。大部分时候是某个 Agent 把上游全文都带上了,而不是带摘要。
5.2 任务异常排查速查表
| 现象 | 常见原因 | 排查路径 |
|---|---|---|
| 任务卡在 running 一直不动 | 模型节点挂了,心跳超时 | 看 watchdog 日志,查最后心跳 |
| 某类任务 token 突然暴涨 | 上下文泄漏,Agent 把全文带入下一步 | 看审计轨迹,查 input_tokens 分布 |
| 模型输出格式不稳定 | 上下文太长,指令被稀释 | 压缩上下文,把格式要求放到最近位置 |
| 两个任务结果互相污染 | 全局状态未隔离 | 查 RunContext 归属,禁用全局字典 |
| 重试后数据重复 | 幂等键缺失 | 在写操作上加请求 ID |
5.3 两条独家避坑技巧
第一条,给 token 做“优先级”而不是只做“上限”。普通的 max_tokens 限制是硬切,容易导致任务输出不完整。我后来引入了优先级预算:低优先级的推理超过预算直接降级到便宜模型继续跑,高优任务超过预算则暂停,等人工决策。这样既保护了核心质量,又不会无限烧钱。
第二条,大模型回复最好流式落地。刚开始我图省事,让模型一次性返回完整 JSON,结果一个超大输出可以直接把进程内存打满。改成流式后,逐行解析、逐字段写入,内存占用平滑很多。
6. 一个人做完这一切,我最后想说的
现在再回看这九个月,我最大的体会不是“一个人能写出 20 万行代码”,而是“一个人怎么控制 20 万行代码带来的复杂度”。
Harness 架构对我而言最宝贵的地方,不是把 Agent 包装得更好用,而是它给了全系统一个可控的支点。无论我在外面接了多少个插件,挂了多少个模型,主流程永远知道任务从哪来、现在在哪、下一步去哪。这种掌控感,才是支撑我一直往下写的动力。
40 亿 token / 月,20 万行代码,最终换来的不是某个可炫耀的数字,而是一个能持续演进、出问题能快速定位、加功能不用推倒重来的一套系统。如果你也在做多 Agent 应用,我的建议很直接:先把骨架打稳,再谈智能。你连接模型的那层代码写得越稳,后面每一步都走得越快。