
先说结论这个东西到现在还在线上跑每个月要烧掉 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。每次任务都是一个 RunRunContext 保存任务的主输入、当前阶段、中间产物和最终输出。任何 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( messagesmessages, toolsavailable_tools, model_routeextract_cheap, # 路由名而不是固定模型名 prioritynormal, )网关内部负责模型路由、失败重试、请求合并、流式转发、自动降级。业务层永远不关心现在到底是哪个模型在处理任务只关心“用哪个路由”。同一时间我上了 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 归属禁用全局字典重试后数据重复幂等键缺失在写操作上加请求 ID5.3 两条独家避坑技巧第一条给 token 做“优先级”而不是只做“上限”。普通的 max_tokens 限制是硬切容易导致任务输出不完整。我后来引入了优先级预算低优先级的推理超过预算直接降级到便宜模型继续跑高优任务超过预算则暂停等人工决策。这样既保护了核心质量又不会无限烧钱。第二条大模型回复最好流式落地。刚开始我图省事让模型一次性返回完整 JSON结果一个超大输出可以直接把进程内存打满。改成流式后逐行解析、逐字段写入内存占用平滑很多。6. 一个人做完这一切我最后想说的现在再回看这九个月我最大的体会不是“一个人能写出 20 万行代码”而是“一个人怎么控制 20 万行代码带来的复杂度”。Harness 架构对我而言最宝贵的地方不是把 Agent 包装得更好用而是它给了全系统一个可控的支点。无论我在外面接了多少个插件挂了多少个模型主流程永远知道任务从哪来、现在在哪、下一步去哪。这种掌控感才是支撑我一直往下写的动力。40 亿 token / 月20 万行代码最终换来的不是某个可炫耀的数字而是一个能持续演进、出问题能快速定位、加功能不用推倒重来的一套系统。如果你也在做多 Agent 应用我的建议很直接先把骨架打稳再谈智能。你连接模型的那层代码写得越稳后面每一步都走得越快。