☰
一人九个月20万行代码:Harness架构多智能体应用实践
2026/10/3 10:58:13 网站建设 项目流程

先说结论:这个东西到现在还在线上跑,每个月要烧掉 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/*各类主流程 Runner3.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 应用,我的建议很直接:先把骨架打稳,再谈智能。你连接模型的那层代码写得越稳,后面每一步都走得越快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询