ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

九个月20万行代码:AI应用Harness架构设计与Token成本治理

九个月20万行代码:AI应用Harness架构设计与Token成本治理 我盯着这个标题看了很久一个人、九个月、20万行代码、每个月烧掉40亿token。这几个数字摆在一起没做过AI应用的人第一反应多半是“吹牛”干过的人第一反应是“这人怕不是被逼疯了”。我属于后者——这套Harness架构应用从零搭到上线我前后正好用了九个月代码量最终也没差多少卡在20万行上下。产品本身不吹今天想认真聊聊这套架构背后的设计取舍、那些token到底花在了哪、以及一个人在这种体量的项目里是怎么活下来的。如果你正在做AI Agent、AI原生应用、或者任何靠LLM长期驱动业务的东西这篇文章应该能给你一点参考。尤其是那些已经受够了LangChain式黑盒、想自己掌控执行流程的人——Harness架构这条路你会很有共鸣。1. “一个人九个月20万行代码”先把标题拆开看1.1 三个关键数字意味着什么先拆“20万行代码”。在传统后端项目里20万行是个不小的规模在AI应用里这个数字其实很微妙。纯粹调API的demo级应用三五千行就能跑起来但如果你要让LLM稳定地调用几十个工具、处理长会话、做缓存和重试、扛住并发和不稳定输出那20万行是必然结果。我的构成里将近六成是各种工具的接入与适配两成是执行循环和调度逻辑剩下的是Web服务、数据库和测试。这还不算第三方库。再看“九个月”。这个周期意味着它不是那种两个周末做出来的玩具而是有用户、有反馈、有迭代压力的真实产品。九个月里我砍掉过至少三个核心功能推翻过一次主架构有一段连续三周每天凌晨两点都在跟同一条报错死磕。至于“一个人”这是最容易被低估的条件。一个人意味着没有沟通成本任何改动从念头到上线可能只要半天但也意味着所有崩溃都压在你一个人身上没有同事能帮你review那三千行刚生成的代码。到了后期我反而觉得一个人反而是Harness架构能跑通的关键——因为这种架构高度依赖你对每一环节的“手感”而手感没法靠开会传递。1.2 Harness架构到底是什么为什么非它不可“Harness”在软件工程里不是新词测试领域有test harness部署领域有deployment harness中文通常译作“马具”或“脚手架”。它本质上是套在核心部件外面的控制装置让一个强大但不可控的东西按你的意愿运作——这正好就是LLM需要的东西。我理解的Harness架构核心是把四个东西像马具一样套在模型外面执行循环agent loop、工具层tools、状态与记忆memory/state、模型路由与成本控制router/budget。模型本身反而只占很小一部分权重。系统里真正值钱的是那层“不管模型给我什么鬼话系统都能继续稳定跑下去”的控制逻辑。为什么不用现成框架我用过LangChain前期确实舒服但到后来越来越难受。一是抽象层太厚出问题你得翻到依赖包内部去查二是它就是不肯让我“直接改那个if else”三是对私有化部署很不友好。后来我明白了AI应用走到一定复杂度之后真正要拼的不是谁的Prompt写得好而是谁对执行链条有绝对控制权。Harness架构就是把这个控制权全部拿回到自己手里。2. Harness架构的主干模块设计与实现2.1 执行循环一个Loop撑起全部智能整个系统的心脏是一个跑在服务端的执行循环。它的逻辑并不复杂伪代码是这样# 简化版 agent loop def run(self, task: str) - str: state self.init_state(task) for step in range(self.max_steps): # 熔断检查token、时间、步数任一超限就强制收尾 if self.budget.exhausted(): return state.summarize() # 模型决策把当前状态工具定义一起交给模型 plan self.llm.decide( state.to_context(), self.tools.schemas() ) # 解析模型输出拿到具体的工具调用或最终答案 action self.tools.parse(plan) if action.kind finish: return action.result # 执行工具拿到观察结果 observation self.tools.execute(action) state.record(plan, observation) # 超过最大步数强制收尾而不是无限循环 return state.summarize()这个循环看起来简单但里面的每个环节都有坑。最典型的是模型输出不合法JSON——GPT系列模型在返回工具调用时偶尔会夹带markdown片段、多余逗号、甚至直接把一段自然语言混进来。所以我在这层用了“双解析”先正则清洗再用容错JSON解析器实在失败的把原文作为错误信息回喂给模型让它自纠。这个机制在实战中救了我无数次。另一个关键是max_steps。没有步数上限的agent你迟早会撞上一次让模型“再来一次”的死循环。我一般设15步上限复杂的多阶段任务拆成子任务分别跑。加上每步的timeout控制整个循环才能做到“无人值守也能自己收场”。2.2 工具注册层让模型“按套路出牌”工具层是Harness架构里最繁琐也最值得花时间的部分。我的做法是写了一个基于装饰器的注册表每个工具都是普通Python函数加上一份描述元数据# 工具注册核心 class ToolRegistry: def __init__(self): self._tools {} def register(self, func, name, description, parameters): self._tools[name] { func: func, schema: { type: function, function: { name: name, description: description, parameters: parameters, } } } def schemas(self): # 返回给模型看的工具定义列表 return [t[schema] for t in self._tools.values()] def parse(self, plan): # 从模型输出中解析出工具名和参数 ... # 用法示例 registry ToolRegistry() def search_web(query: str, max_results: int 5): 调用搜索接口返回列表 ... registry.register( search_web, nameweb_search, description搜索引擎查询。当用户问实时信息、新闻、最新资料时使用。query是搜索关键词短句即可。, parameters{ type: object, properties: { query: {type: string, description: 搜索关键词}, max_results: {type: integer, description: 返回条数默认5, default: 5} }, required: [query] } )这里有一点非常关键工具描述的措辞直接决定模型选不选得对。我自己踩过很大的坑——早期一个“查天气”的工具描述写得含糊模型经常在用户问“今天要不要带伞”时去调用搜索引擎而不是天气接口。把描述改成“根据城市名获取实时天气用于判断穿衣、出行、是否需要带伞”之后准确率立刻上去了。工具描述要写“什么时候用”而不是只写“这个工具干什么”。另外参数尽量扁平化不要用嵌套对象嵌套会让模型生成合法JSON的成功率显著下降。2.3 记忆与上下文管理LLM应用最难处理的问题之一就是上下文。模型窗口再大也是有限的而且每多塞一token都是在烧钱。我的做法是分两层短期记忆留在上下文中长期记忆落在数据库和向量库里但绝不把整个历史原封不动塞回去。每次工具调用返回的原始结果我会先做“提炼”。比如搜索工具返回了10条网页摘要我只把前3条有相关性的摘要和一个来源链接存进上下文而不是把整页HTML、整段原始输出丢给模型。这个过程我起名叫“观察压缩器”。另外会话超过一定长度之后系统会对早期对话做一次摘要用一段几百字的摘要替换掉几千字的原始历史把上下文维持在一个稳定的水准。这里要补充一个常识所谓上下文窗口不是说你写到多少token就报错而是当接近上限时模型质量会肉眼可见地下降它开始“忘记”你早先给的指令。等到报错再去处理就晚了。所以我会把运行时的token上限设定为模型窗口的70%剩下的30%留给输出和突发。2.4 模型路由与降级策略二十万行代码里最没想到会这么复杂的是路由层。我原本以为一个GPT级别的大模型就能对付所有场景后来发现完全不行。简单任务用大模型纯粹是浪费复杂任务用小模型又确实干不了。所以系统跑起来后我专门加了一层路由器规则大概是这样任务类型使用模型策略意图分类、关键词提取轻量小模型追求速度和成本单步问答、工具结果总结中型模型平衡质量与费用复杂推理、多步规划的首次决策旗舰大模型保证首轮质量重试、降级、失败后兜底小型模型简化prompt宁可降级不可中断路由层还有降级逻辑调用大模型失败或超时时自动把请求降级到中型模型并用一个更简单的prompt模板代替复杂模板。代价就是回答质量略有下降但至少用户不会被挂在一个无限转圈的加载页上。这个“保底”思维在一个人维护的系统里几乎是生存必需品。3. 九个月都干了些啥20万行代码的真实构成3.1 时间线复盘MVP、工具链、稳定性、打磨第一、二个月我做的就是纯MVP一个能跑通“用户提问→模型调用两三个工具→给出答案”的最小闭环。这时候代码量大概只有两万行架构也没那么复杂——就是一个写死的流程加几个工具函数。回头看这个阶段最大的价值不是代码而是逼我把“模型输出不可靠”这件事彻底认清了。第三、四个月进入工具暴涨期。各种搜索、数据库、文件处理、API对接工具像滚雪球一样增加注册表上挂了几十个工具。代码量开始飙升但这时候也暴露出架构的第一个问题工具之间的依赖关系没有建模有些工具的结果本该传给下一个工具当参数我却只能靠模型自己“理解”。后来我加了一个简单的“工具链”声明机制让工具作者也是我自己显式指定前后置关系准确率才稳定下来。第五、六个月是全项目最痛苦的一段——稳定性。我大量重写重试逻辑给每个外部调用加超时和退避给所有写操作加幂等键给对话状态做持久化。那段时间每天都在跟“偶发性的奇怪行为”搏斗后来发现绝大多数问题都出在同一个地方状态没有统一管理。于是我把会话状态全面收敛到一套显式的状态对象里任何模块改写状态都要走统一的接口。第七、八个月是扩展期加了多租户、后台异步任务、更细粒度的权限。第九个月基本没加新功能全在压测、修边界问题、做成本优化。整个节奏其实很传统先跑通再变厚再变稳最后变省。时间线上没什么捷径。3.2 代码量统计与AI辅助开发的比例20万行代码里大约70%是在AI辅助下写的。这听起来很快但有个很多人没说透的事实AI生成的代码成本不在“写”的时候而在“重构”的时候。初期我用AI生成代码非常奔放它给什么我贴什么结果到第四个月系统已经出现明显的“代码膨胀”——同一个功能有三套实现一个bug在三个地方各修了一遍改动一个工具就连带出现两个不相关模块的回归。后来我调整策略AI负责生成可运行的第一版但强制要求自己每周做一次“人类重构”把AI代码里那些绕弯子的逻辑捋直。有一说一AI写的代码在处理常规业务逻辑时很靠谱但涉及跨模块状态、并发、异常恢复这类敏感地带人类判断依然是刚需。代码量的真实构成大概是这样的一个分布模块占比说明工具集成与适配60%每个外部系统都要写认证、错误码翻译、限流处理执行循环与调度20%agent loop、路由、重试、熔断、状态机Web服务与存储10%API层、会话管理、数据库访问测试与压测脚本10%回归测试、mock外部服务、并发压测工具集成占了六成这可能是很多人没想到的。每一个外部API哪怕文档再干净都要处理认证过期、限流、字段缺失、时区差异等一堆现实问题。20万行里没有多少“AI魔法”绝大多数都是这种与工程现实肉搏的代码。3.3 一个人的工程化底线一个人开发最危险的想法就是“我不需要工程化因为只有我能看懂代码”。九个月下来我的体会是工程化不是为了协作是为了让你自己两个月后还能改得动。我的底线是三样测试、日志、告警。测试我用pytest核心执行循环和工具解析层是重点覆盖对象尤其会mock掉所有外部API保证测试可重复跑。日志统一走结构化格式JSON输出每一条都带request_id和trace_id。告警方面我只做了三个最关键的单次运行步数超限、单日token预算消耗超阈值、任务失败率超过5%。这三个告警救了我好几次尤其是最后一个——有些故障是慢性的不是当场炸而是让你某个功能成功率悄悄往下掉。4. 每月40亿token钱花在哪怎么省4.1 先算一笔账40亿token是什么概念每月40亿token摊下来每天大概1.3亿再按并发和请求量看平均每个请求约消耗几千token。这个体量意味着它背后不是一个demo而是真有用户在生产环境里长周期使用。换句话说token不只是模型在“思考”时烧的更多是系统运行的结构性开销。按常见API价格粗算一笔账假设混合均价每百万token在8美元左右输入输出混合、大小模型混合40亿token就是每月3200美元到数万美元不等。具体看折扣和路由策略。我见过很多团队成本失控根源就是压根没意识到“token也是需要做预算管理的资源”。我给自己建了一张token去向表每月拉一次统计去向占比备注主对话与工具决策40%模型做每一步规划的输出输入工具返回与上下文重放30%搜索引擎返回、文件内容等喂回给模型会话历史累积20%长会话的历史记录重放评估与压测10%自动化测试也会真实消耗token这个表最扎心的发现是真正直接产生价值的“模型规划”只占四成剩下六成都是在为上下文和服务兜底买单。所以省钱的着力点非常清晰要么压上下文要么压重放。4.2 上下文缓存与压缩最容易被忽视的省钱点很多人在算token成本时只算了每一次请求的input和output却忽略了同一会话里重复发送的system prompt和固定工具描述。这些前缀其实每轮都在重发积少成多非常吓人。开源方案是加prompt caching。当你的system prompt和工具定义的文本前缀完全一致时很多平台支持按缓存价格计费便宜不少。前提是你要把“固定的东西”和“动态变化的东西”彻底分开固定前缀放最前面每次请求绝不改动用户相关的上下文放在后面该变就变。这个结构调整我还专门写了测试去断言固定前缀在运行时不会被意外改动。另一个大利是压缩工具返回。我在2.3里提过“观察压缩器”它在省钱维度上价值巨大。同样一次搜索把10条结果的全文都塞进去和只塞3条提炼过的摘要token开销可以差到5倍而回答质量几乎没有可感知的差距。所有工具在注册前我都会问自己一个问题模型真正需要这个工具返回的哪些字段只保留这些。4.3 模型分层与语义缓存分层路由本身也是省钱的大头。同类任务在小模型能完成的情况下我尽量不调用旗舰大模型。一个很典型的例子用户的闲聊和重复性问法用中小型模型完全可以应付只有真正需要多步推理的任务才升级到旗舰模型。这个策略让我的账单降了至少一半。另一个容易遗漏的是“结果缓存”。同一问题在短时间内被反复问到比如两个用户问一模一样的政策条款系统应该直接返回上一次的结果连模型的边都不沾。我的语义缓存很简单把用户输入做embedding跟近期问题比对相似度超过设定阈值就复用旧答案。对于信息类、咨询类的场景这个命中率相当高长期跑下来省下的token非常可观。5. 高频故障与排查实录token报错、循环死锁、成本爆炸5.1 token类报错速查表九个月里我跟各种token类报错打了无数次照面整理成一张速查表遇到同类问题基本能快速定位报错信息现象根因处理方式invalid api key / 401 unauthorized请求直接被拒API Key失效、权限被吊销检查配置中心里的key、确认是否被重置用服务账号轮换机制定期换keytoken exchange failed: 403 forbidden登录/接入时返回403区域策略拦截服务支持范围之外切换合规可用的接入点或服务商不要把服务部署在不支持的节点上再强行绕refresh_token为空刷新token失败初始授权未正确保存刷新凭据检查持久化逻辑确保授权回调后先落库再返回必要时强制重新登录sign-in could not be completed前端无法建立会话回调地址、临时授权码不匹配核对回调URL是否在控制台登记过、是否与发起时一致context length exceeded请求载荷超窗口历史记录/工具结果膨胀上线观察压缩器把最大会话步数调低早做摘要替换codex auth token unavailable命令行/API无法取到令牌本地token未被正确写入环境变量检查CLI配置里的令牌存放路径重新执行设备认证流程这里需要多说一句区域策略拦截。出现403不一定是你代码有bug很可能就是服务商本身有服务范围限制。合规的处理方式不是想办法去绕过而是直接把运行节点或服务商切换到支持该区域的合法方案上或者干脆换一家覆盖范围更合适的服务。这既是稳定性问题也是底线问题别在这种事上动脑筋。5.2 上下文失控与工具调用死锁最让我头疼的一类问题是“看起来在干活实际已经死了”的循环状态。模型会一直调用同一个搜索工具把同一个问题搜了八遍每次都返回差不多的结果然后继续搜。原因是工具返回的“无新信息”没有有效反馈给模型它以为再搜一次就能找到答案。我的解法是两层一层是步数上限前面说过的max_steps硬性掐死另一层是检测重复动作——如果模型连续三步调用同一个工具且参数几乎相同系统会主动介入往上下文里插一条系统消息“你已重复执行相同操作请停止结合已有信息作答或告知用户需要更多输入。”这一招极大地减少了无效token消耗也让流程更像个正常人。另一种上下文失控是历史越攒越多。早期我把每次对话的原始记录都留在上下文里结果第三天就撞了窗口上限报错后整个会话状态丢失。后来加了个机制会话每到20轮就自动对最老的10轮做摘要摘要结果替换原文保留整个会话长度维持在可控范围内。这个功能上线后长会话的稳定性明显上了一个台阶。5.3 成本失控的“熔断机制”我真正意义上被token账单吓到过。某天因为上面说的重复搜索循环bug一晚上烧掉了原计划一整天的预算账单数字跳出来的时候我血压都上来了。人眼盯着日志抓这种问题根本来不及所以我直接做了预算熔断机制。# 预算熔断每日token计数器 class BudgetGuard: def __init__(self, daily_limit): self.daily_limit daily_limit self.consumed load_from_redis(token:consumed:daily) def exhausted(self): return self.consumed self.daily_limit def before_call(self, estimated_tokens): if self.consumed estimated_tokens self.daily_limit * 0.8: # 超过80%时降级到小模型 self.router.force_small_model() if self.consumed estimated_tokens self.daily_limit: # 超过100%时直接拒绝非必要调用 return False return True熔断逻辑不复杂设定每日token上限跑到80%时自动降级到小模型到100%时停止调用并给用户返回明确的兜底提示。这个机制上线之后再没出过预算惊吓。给所有准备做AI应用的人一句忠告成本控制不是财务问题是系统架构的一部分必须写进代码里而不是月底看报表。6. 给准备走这条路的人几句实话一个人写20万行代码不难难的是每天坐在同一个工位前面对一堆报错日志和token消耗账单还能保持思路清晰。九个月里我最大的体会是Harness架构真正的价值不是“显得很专业”而是让你在无人值守、模型疯狂抽风、用户连环催的情况下依然敢让系统连续跑上百步而不崩。它就像马具一样把那匹精力旺盛但随时会乱跑的“模型马”勒在你规定的路线上。如果你想做类似的AI应用我的建议是别一上来就搭Harness。先把业务闭环用裸调API跑通认真记下每一处让你睡不着觉的不稳定点再回头用Harness架构把这些问题一个个装进去。先有痛点后有架构顺序反了你只是在用复杂度掩盖空虚。这套架构后续能扩展的方向也很多比如多租户隔离、更细粒度的权限控制、跨会话的长期记忆。但这些都是后话——先把第一版跑稳把账单看住再谈远方。
返回列表