
一个人、九个月、20 万行代码、每个月烧掉 40 亿 token——这几个数字摆在一起很多人第一反应是“又一个 AI 项目的 PR 稿”。但这是我实打实做出来的一个 Harness 架构应用一个不依赖现成 Agent 平台、自己实现模型调度、工具调用和任务编排的 AI 执行系统。它不是聊天机器人也不只是 RAG 问答而是一个能接收目标、拆解步骤、调用代码和外部工具、循环迭代直到交付结果的“数字员工”。这篇文章我想把整个项目的思路、架构细节、成本真相和踩坑过程完整拆给你看尤其适合正在做 AI Agent、想自己搭工具调用框架、或者被 token 成本搞得睡不着觉的开发者。我花了很多时间在“如何让大模型稳定地干活”这件事上而不是“如何让大模型说话”。这也是 Harness 架构和普通聊天应用最本质的区别。整个项目从零开始代码规模最终到了 20 万行主要包含 Python 写的调度核心、Go 和 Node 写的工具执行器、前端任务面板、状态数据库和一套完整的测试/可观测性体系。九个月里我几乎每周都在重构调度循环直到最后三个月才真正稳定下来。下面我会把核心设计、token 消耗构成、以及我亲测有效的优化手段都讲清楚好东西在后头。1. 我做的到底是什么为什么非要用 Harness 架构1.1 从聊天机器人到自动干活的“执行系统”如果你做过 GPT-4o、Claude 这类模型的接入最熟悉的场景是“一段 prompt 进去一段回答出来”。这本质上是一个无状态函数调用。但真实世界的任务比如“分析这份销售数据并生成一份 PDF 报告”不是一次对话能完成的它需要先读取文件、清洗数据、跑 Python 回测代码、调用图表库、反复修正格式、最后把结果渲染成文档。每一步的结果都会影响下一步怎么做这就需要一个外部结构去承载循环接收模型输出、执行具体动作、把动作结果再喂回模型。这个外部结构就是 harness业内也常叫 agent loop 或 tool-use loop。我自己实现的 Harness 不依赖 LangChain、不依赖 CrewAI 这类上层框架而是直接用原生模型接口实现了四件事规划器、执行器、观察器、再规划器。规划器负责把大目标拆成步骤执行器负责真正调用工具观察器负责把执行结果整理成模型能消费的文本再规划器根据新信息决定是继续、重试还是结束。这就是一个非常朴素的“感知-决策-行动”循环。很多同行问我为什么不直接用现成框架。答案是我发现框架最多能省前期的几千行代码但当你需要精细控制 token 预算、工具并发、错误恢复、断点续跑时你最终还是要深入到框架内部去做定制那个成本往往比从零写还高。九个月下来我自己维护的调度核心大概只有 1 万行但它支撑了 20 万行的工具生态和业务逻辑这是框架给你的“自由度”换不来的。1.2 Harness 架构与应用外壳的区别这里我要特别强调一个容易混淆的点很多人把“应用外壳”当成“Harness”。外壳只做两件事把用户输入发给模型、把模型输出显示出来。Harness 则要管理模型之外的完整生命周期任务状态机、工具的注册与鉴权、上下文的裁剪与摘要、重试策略、并发控制、审计日志、成本计量。一句话外壳服务用户Harness 服务任务本身。我做的这套应用里每个任务从一开始就进入状态机。状态包括 pending、planning、executing、waiting_for_tool、retrying、succeeded、failed。每一个状态都有明确的持久化记录进程崩溃后可以从数据库里恢复。这比“发一次请求、等一个响应”的聊天模式复杂得多但好处也很明显用户可以随时关掉页面任务在后台继续跑跑完后推送通知。这种体验如果只靠外层包装是做不出来的。Harness 的另一个关键职责是扮演“纪律员”。模型偶尔会输出不符合规范的内容比如工具参数漏了必填字段、JSON 里混入注释、返回一个不存在的工具名。harness 要做的是不能直接崩溃而是构造一个“结构化纠正信号”重新发给模型让它修正。这个重试循环最多五次超过就进入失败状态并通知管理员。前三个月我把大量时间花在这种异常路径上后来证明这些“不性感”的代码恰恰是系统的可靠性来源。1.3 这二十万行代码都花在哪了很多人听到 20 万行代码会吓到但我拆开算过真正的核心调度不到 10%剩下的都是“让模型能做到具体事情”的工具。我大致盘点一下构成Python 服务端核心任务队列、Agent 循环、上下文管理、认证模块约 2 万行。工具执行器每个工具平均 100 到 800 行覆盖文件读写、数据库查询、网页抓取、代码执行、Office 文档生成、邮件发送、IM 通知等总共 80 余个工具约 8 万行。前后端任务面板、日志流、token 用量看板、工具调试界面约 4 万行。测试与基础设施单元测试、集成测试、CI 脚本、Docker Compose、部署脚本、监控告警约 2.5 万行。各类数据模型、迁移脚本、文档、示例配置约 3.5 万行。一个人写这么多代码最大的危险不是写得慢而是“烂在自己手里”。我的处理方式是尽量让模块之间只通过接口通信工具之间绝对不允许互相 import 业务逻辑每个工具都有独立测试代码里每条关键路径都要有 trace_id。这样即使有一段时间不维护某个模块我回来时也能快速定位问题。如果你也在做类似项目我建议你从一开始就配好日志和测试不要等代码量上来以后再补那个成本会高到你想放弃。2. Harness 核心设计让模型按你的框架跑2.1 任务生命周期的四个阶段我在设计一次任务的完整流程时没有直接采用“让大模型自由发挥到完成”的方案因为那样 token 消耗不可控且模型很容易在中间迷失方向。我的任务是四阶段循环第一阶段是目标解析把用户的自然语言变成一个结构化的任务描述包括成功标准、交付物、允许使用的工具范围第二阶段是规划模型输出一个有序的步骤列表每个步骤都绑定预期产出第三阶段是执行系统按顺序执行步骤如果某一步依赖前一步的产物执行器必须显式等待第四阶段是验证模型根据最终产出和成功标准进行自省不满意就重新进入规划阶段最多迭代三次。这里我用了一个比较实用的技巧第一阶段尽量用便宜的小模型完成比如目标解析、意图分类、工具范围判断这些不需要很强的推理能力。真正需要复杂推理的规划和验证才调用旗舰级模型。一个小模型可能每次任务只花几百个 token但一个月下来能省出的成本非常可观。每个任务跑完以后系统会保留一份“执行轨迹”包括每个步骤的输入输出摘要、模型调用的 token 数、工具执行耗时、失败原因。这不仅是审计需要更是后续优化的数据基础。我每个月都会统计哪种任务在哪个步骤最容易失败然后针对性调整工具说明或提示词。这相当于用数据驱动的方式给系统做“持续调教”。2.2 让模型输出可执行的结构Harness 的核心难点之一是让大模型“吐出来的东西”能被机器稳定解析。我的方案很直接强制结构化输出。所有规划器调用统一使用 response_format要求输出一个 JSON StepList。每个 Step 包含 step_id、description、tool_name、tool_args、depends_on 五个字段。如果模型输出无法通过 Pydantic 校验就把校验错误原文发回给模型让它重试。下面是一个非常简化的规划输出示例{ steps: [ { step_id: step_1, description: 读取销售数据目录, tool_name: list_files, tool_args: {path: /data/sales, pattern: *.csv}, depends_on: [] }, { step_id: step_2, description: 运行月度汇总脚本, tool_name: run_python, tool_args: {script: scripts/monthly_summary.py, input: {{step_1.output}}}, depends_on: [step_1] } ] }模板变量{{step_1.output}}是我一开始就定下的约定在执行阶段会被真实输出替换。这个设计的最大好处是依赖关系在规划阶段就明确了执行器可以并行跑没有依赖关系的步骤显著缩短任务总时长。但缺点也很明显模板语法限制了模型的表达力有些复杂任务模型想灵活编排时会觉得束手束脚。后来我加入了“动态步骤”机制允许工具返回一个新的子计划嵌入主流程相当于给模型一个“临时改方案”的出口。对于工具调用我统一要求模型生成 JSON 参数不鼓励模型自己拼接命令字符串。因为字符串拼接极易出错而且没有类型校验。所有工具参数都定义在 JSON Schema 中比如字符串最小长度、枚举范围、文件路径格式等。模型输出先过 schema 校验再过一层安全校验比如数据库工具只允许 SELECT代码执行工具有独立的沙箱用户和 CPU 时间限制。任何一步不过直接拒绝执行并返回原因。2.3 工具注册中心把系统能力“翻译”给模型工具注册中心相当于 harness 的“神经末梢”我的架构里每个工具都是一个独立的插件包。工具定义包含三部分human_name、description、parameters。描述部分我花了大量心思因为模型的工具选择正确率很大程度上不取决于参数 schema 写得多精确而取决于描述是否能让模型准确理解“什么时候该用这个工具”。比如一个“查询客户详情”的工具我不会只写“Get customer info”而会写成“根据客户 ID 获取基础资料、最近 30 天订单金额和客服工单数。适用于订单管理、对账、客户分群等场景。若仅需订单列表请优先使用 query_orders”。这样模型在规划阶段就能更精准地把任务意图映射到工具上。八十多个工具我花了整整三周反复打磨描述后来的任务成功率提升了接近三成这比换更强的模型划算得多。工具的输出也需要统一封装。我要求所有工具返回值必须是可 JSON 序列化的且不能超过一定大小超过的部分会自动截断并返回截断提示。对于文件内容这类长度不可控的输出我会执行器先写入临时文件再让工具返回文件路径和行数信息模型需要时再继续读。这样一来一次工具调用的输出 token 就能被严格控制住避免模型上下文被一次 ls 命令的结果撑爆。3. 一个人九个月最难啃的三块骨头3.1 上下文窗口是稀缺资源Token 预算分配如果你以为 Harness 架构最难的是让模型调用工具那你就太天真了。我踩过最大的坑是上下文失控一个任务跑到第 12 轮工具调用时历史消息加工具结果已经超过 100k token模型开始“忘记”最开始的目标甚至开始编造之前工具返回的数据。所以我在系统的每个循环周期里都做了一次“Token 预算分配”。我的核心思路是给每次模型调用设一个硬性预算系统提示固定分配 1500 token最近两轮对话保留 4000 token工具结果最多带 6000 token更早的历史统一压缩成摘要摘要最多 2000 token。超出预算的部分不是简单丢掉而是交给一个专门的摘要模型生成结构化要点包括“已确认事实”“未完成目标”“待验证假设”。这样模型每次看到的都是当前最该关注的信息而不是被一堆原始日志淹没。这个策略上线后模型幻觉率明显下降任务失败率几乎减半。代价是摘要模型本身也要消耗 token后来我把摘要模型也降级成中档模型用几天时间验证它对最终结果的影响不大才算找到一个性价比平衡点。3.2 不可避免的错误恢复机制Harness 应用里没有任何一个环节能保证 100% 正确模型会错、工具会挂、网络会断。我的目标不是消灭错误而是让系统在错误发生后能用最小的成本自我修复。为此我实现了三层错误恢复第一层是工具级重试适合超时、瞬时网络抖动默认最多重试 2 次退避时间按指数增长第二层是规划级重试当工具被确认不可用时把错误信息喂给规划器让它换一种工具或换一个方案第三层是任务级回退如果连续多次修正仍然失败任务不会直接判死而是回到“人工接管”状态等待管理员补充上下文后再继续。设计这套机制时我犯过一个典型错误一开始只要工具调用失败我就让模型重新规划结果模型在同一个错误上反复打转白白烧掉几百万 token。后来我加了一个“失败指纹”机制把工具报错的关键信息做哈希同一任务里如果同一个指纹出现超过三次就不再允许模型对它重试而是直接上报人工。这个改动让整体 token 消耗下降了约 18%非常可观。我还特别强调了一点所有错误恢复路径都必须有日志。因为模型在重试时可能做出各种不可预测的修改如果没有完整留痕出了事故根本无从追踪。每一个任务请求都带 trace_id每一条日志都记录模型返回的 token、工具执行的返回码、重试次数、耗时最后统一汇总到任务的 timeline 视图里。后面排查问题的时候这个设计帮我省了非常多时间。3.3 并发、限流、Checkpoint让系统跑得更久单任务的 Harness 跑通不难难的是同时跑几百个任务。我的任务队列用的是数据库行级锁加内存队列的两级方案数据库负责持久化任务状态内存队列负责分发待执行的步骤。每个模型端点都有独立的限流器按 token 速率和并发双维度控制超出预算的任务先排队而不是立刻把 API 打爆。并发带来的最大隐患是“状态撕裂”。比如一个任务在等待工具结果时另一个任务把系统配置改了或者同一个文件被两个任务同时写入。我的对策很土但很有效所有共享资源统一加租约lease文件操作重定向到每个任务专属的临时目录数据库写入走事务。配置项在任务启动时快照一次整个生命周期内不会热更新。这套约束限制了系统的灵活性但换来了确定性对一个单人维护的项目来说确定性远比自由度重要。Checkpoint 是让我敢睡觉的基础。每个任务在每个步骤完成后都会把完整状态、上下文摘要、中间产物路径持久化到数据库。服务重启后会扫描处于 executing 状态的任务统一回退成“刚完成上一步”的状态重新调度。当月度 token 限额告警触发时系统也能暂停新任务但不会中断已有任务的 checkpoint 记录等配额恢复后续跑。还有一点所有长时间运行的任务我都会在代码里设置每步最大执行时长超过就直接判失败防止一个卡死的工具调用无限耗尽资源。4. 40 亿 Token 的账单拆解与省钱实战4.1 这 40 亿 Token 是这么烧掉的每个月烧掉 40 亿 token听起来是一个很夸张的数字但如果拆开看就很合理。我把一个月作为成本分析周期所有任务混合在一起Token 消耗大致分成五类消耗类型占比说明系统提示与工具定义8%每次调用都会带上量大但单次固定规划与目标解析15%任务开始时的高消耗阶段工具结果回填38%最大头文件内容、数据库查询、截图转文本都算这里上下文历史与摘要27%多轮循环中反复携带历史虽然有摘要但仍有成本最终交付物生成12%报告、PPT、代码等最终产物在这个消耗结构里可以看到工具结果回填和上下文历史加起来占了 65%。也就是说大头不是“模型思考”而是“模型读取信息”。所以省钱的重点不是让模型少思考而是让它少看无用信息。我粗略算过一笔账如果 40 亿 token 全部走旗舰模型每月成本会非常吓人可能超过六位数人民币。但实际项目里我做了一个非常关键的成本控制把所有读请求优先分配给中档模型只有需要复杂推理时才用旗舰模型。再加上 prompt cache 命中带来的折扣最终的费用大概控制在纯旗舰成本的 30% 到 40% 之间。这种混合路由策略是我建议每个做 Agent 的人都认真研究的省钱方向。4.2 我的三个省 Token 实战策略第一个策略是优先缓存稳定前缀。系统提示、工具定义、用户元信息这些内容每次调用都一样我会让它们永远排在消息序列最前面然后开启厂商的 prompt cache 功能。这样每次调用这部分 token 的价格会大幅下降而且不占实际推理负载。如果你的模型接口不支持缓存那就自己做一个重复内容的占位符机制用一次性填充的方式减少传输量。第二个策略是工具输出三段式压缩。任何工具返回数据都先进入压缩器第一段保留字段名和数据形状第二段保留 top 10 条记录样例第三段只保留聚合统计。比如一个工具本来返回 3 万行日志真正进入模型上下文的可能只有 1500 token 的结构化摘要。如果模型判断需要完整数据可以再显式调用 fetch_tool_output 去取。这相当于给模型配了一个“先看摘要再找原文”的能力非常省 token。第三个策略是任务级批量。多个相似任务比如“给 30 个客户写日报”我不会让系统开 30 个完整任务而是先把它们聚合到同一个任务里共享一份系统提示、一份工具定义和一个全局的业务背景然后让模型逐一处理。这能省下大量重复的输入 token。实测下来批量聚合后单客户处理成本下降了约 40%。4.3 成本监控与“烧钱”安全阀一个人开发最怕的是成本失控模型在后台跑了一个通宵第二天起来账单多出一个零。所以我做了一个很简单的“安全阀”机制每个任务开始时预估 token 消耗上限超限直接熔断每个 API key 配置月度预算按天粒度分摊超过日限额后新任务进入等待状态还有每小时的实时用量看板显示当前时间段的 token 消耗速率和剩余额度。这套机制的代码量不大但它是整个系统能放心跑起来的基石。实时看板上我还加了“单位任务成本”的指标不只看总数。因为任务数量增长时总成本必然上涨真正需要盯的是单次交付的平均 token 数。这个数字稳定或下降说明系统效率在提升如果它持续上涨通常意味着工具输出变长了或上下文压缩失效了需要立即排查。还有一点所有测试跑批尽量用低成本的模型或直接 mock 工具返回不要拿真实 API 做单元测试这是几乎每个开发者都会忽略的隐性烧钱点。5. 实操中踩过的认证、签名与代码导航的坑5.1 Token Exchange Failed 系列错误排查我的 Harness 应用接了大量第三方工具所以对认证错误的处理特别有经验。常见的一类错误是登录或授权时提示sign-in could not be completed token exchange failed: error sending request、token exchange failed: token endpoint returned status 403 forbidden以及failed to refresh token: 400 bad request: invalid refresh_token: empty string。很多人看到这段英文就慌了其实它的意思很直白OAuth 或 OIDC 流程中你拿授权码换 access token或者拿 refresh token 换新 token 时token endpoint 返回了错误。我的排查顺序是固定的。第一步看日志里的失败发生在哪个环节是授权服务器不可达还是 token endpoint 校验失败。error sending request多数是网络层的问题比如临时超时、DNS 解析异常或者防火墙没有放行对应的域名。第二步检查 OAuth 配置参数client_id、client_secret、redirect_uri 是否和你在服务商后台登记的一致尤其是 redirect_uri只要差一个斜杠或多了个 query 参数都会失败。第三步检查 scope 是否超出应用授权范围有些平台在 scope 不合法时会直接拒绝 token exchange。第四步看服务端返回的具体错误码如果 403 里带country, region, or territory not supported那说明账户注册地区和服务支持范围不匹配你需要先解决账户所属区域的问题从合规渠道申请或改用官方支持的注册信息而不是绕过限制。下面是我整理的快速排查表报错片段常见原因优先检查项error sending request网络超时/域名不可达/代理干扰服务健康检查、超时配置token endpoint returned 403地区/国家限制、client 配置被风控账户支持区域、OAuth 配置一致性invalid refresh_token: empty string刷新令牌为空/已被消费刷新流程是否并发执行、令牌持久化your access token could not be refreshed用户登出、refresh token 失效检查会话状态与 token 轮换策略如果你用的是 JWT 类 token还有一个特别容易忽略的问题服务器时钟偏差。JWT 的exp和nbf依赖时间不同机器之间差几秒钟就可能导致 token 被判无效。我在排查 401 时总会先同步一次系统时间这个动作治好了很多怪问题。5.2 JWT 续签、Cookie/Session/Token 选型很多 Harness 应用需要在服务端访问用户的三方账号这就涉及到 token 的长期管理。我最终采用的是双 token 方案简单说就是短时间内有效、可放在前端的 access token加上长期有效、只保存在服务端的 refresh token。Access token 默认 15 分钟过期refresh token 7 天过期同时启用 refresh token 轮换机制每次刷新后旧的 refresh token 立即作废发一个新的。这样即使 refresh token 泄露泄露的令牌也会很快失效。实施时有个坑当多个请求并发遇到 401会同时去刷新 token导致旧的 refresh token 被第一个请求消耗掉后面所有请求都拿到invalid refresh token的报错。解决办法是加一个单飞singleflight模式同一时刻只允许一个刷新请求在途其余请求等待结果或重试一次而不是自己也去刷。另外一个容易忽略的点是刷新流程里一定要校验 refresh token 的token_type、client_id归属防止把 A 应用的 refresh token 用在 B 应用上。至于 Cookie、Session、Token 怎么选我的判断标准是这样的服务端渲染的 Web 应用用 Cookie 更省事靠 HttpOnly 和 SameSite 属性防住大部分 CSRF纯 API 服务用 JWT 或不透明 token 都行关键是把 token 放内存或安全存储不要塞到 localStorage 里裸奔需要跨域多端访问时token 方案比 Session 灵活但要自己处理续签和吊销。我在项目里同时用到了三种方式用户登录走 JWT后端服务之间走 mTLS 加短时 token三方 API 走 OAuth refresh token。每一种都有各自的边界搞清楚了就会很顺手。5.3 交付安全与代码签名Harness 应用经常会生成可执行脚本、安装包甚至桌面应用给最终用户。Windows 用户下载 exe 时如果没有有效的数字签名SmartScreen 会提示风险杀毒软件也可能直接隔离macOS 的 app 没有签名连启动都会被 Gatekeeper 拦住。这给我的交付流程带来很大麻烦。所以我在本地和 CI 里都加入了代码签名环节。证书我选择的是 Certum 代码签名证书主要看中它支持标准的 SHA-256 签名算法、提供硬件令牌选项价格也符合单人项目的预算。Windows 侧用signtool.exe进行签名关键参数包括/fd SHA256、/tr指向 RFC 3161 时间戳服务器/td SHA256因为只有带时间戳的签名才能在证书过期后依然有效。macOS 侧用 Developer ID Application 证书配合codesign和notarytool做公证这一步不能省否则用户打开时还是会弹“已损坏”或“无法验证开发者”。CI 里的签名自动化需要注意证书私钥的保护我建议严格做到通配符路径下看不到私钥签名过程只在专用的 runner 上执行并在配置里开启审计日志。另外每次改代码后重新打包最好用统一的版本号控制并把校验和发到文档页面。签名后的程序也要在干净环境测一遍有些加壳或混淆工具会破坏签名导致启动失败这是我在交付前踩过的隐蔽坑。5.4 大仓库代码分析别让 VCS 卡死最后聊聊代码分析与导航的问题。我的 Harness 应用也会被用来分析大型代码仓库比如“帮我梳理这个微服务项目的模块依赖并生成一份架构说明”。一开始我直接用 shell 工具在仓库里跑 grep、find、git log结果在几百万行代码的仓库里经常卡死甚至把宿主机 IO 打满。单纯利用 VCS 命令做扫描和定位确实容易陷入性能陷阱。解决思路很朴实先建索引再分析。那段时间我把扫描流程改成三步走第一步用脚本生成文件清单和语言统计第二步构建调用图的静态索引第三步让模型只读取索引结果和关键文件而不是命令输出。Ripgrep 比基础 grep 快很多但在超级大仓库里也要限制并发和超时。执行器里跑进程必须设 timeout否则一个失控的find就能拖垮整个系统。后来我还加了一层“路径白名单”复杂查询强制走索引服务减少了对原始 VCS 进程的反复调用才算根治了卡死问题。6. 关于一个人干完这些事我的几句真话说实话一个人做九个月赶上一个小团队的产出量靠的绝不是“每天工作 16 小时”这种悲壮叙事而是靠两条非常朴素的纪律第一每天写完的代码必须在第二天还能被自己看懂第二所有新功能上线前先写失败场景列表。项目能做到这个程度是这些笨功夫堆出来的。中间也有过想放弃的瞬间尤其是第三个月发现上下文管理设计有根本缺陷连续一周每天烧掉几百万 token 却得不到预期结果的时候。现在回头复盘我最庆幸的不是写了 20 万行代码而是每行代码都带着明确的用途和边界。token 从早期被大量浪费到后来形成一套成熟的预算、压缩、缓存和路由体系这个过程让我真正理解了一件事在大模型应用里代码量不是核心资产系统对 token 的利用效率才是。最后分享一个很实用的小技巧给每个任务、每次模型调用、每个工具执行都生成一个 trace_id并且把所有日志都串到这条 trace 上。这个习惯在我排查那些“偶发失败”的问题时效率至少提升了十倍强烈建议正在做 Agent 的开发者第一天就把它落地。