首页 / 资讯中心 / 文章详情

Harness架构实战:一个人九个月写20万行代码与40亿token的取舍

Harness架构实战:一个人九个月写20万行代码与40亿token的取舍 ★ FEATURED ARTICLE
去年我给自己定了个几乎不可能完成的目标一个人用九个月时间写出一款基于Harness架构的应用顺便把代码量堆到20万行。现在回头看最难的不是写代码而是每个月要烧掉40亿 token跟模型“对话”烧出来的钱和心智比任何一行代码都贵。这篇文章不晒项目截图只讲清楚三个问题Harness架构到底是什么、一个人怎么扛下20万行代码、token到底烧在哪。1. 先把“Harness架构”说人话1.1 为什么叫Harness不叫Framework或Platform“Harness”这个词玩过攀岩、跳伞的人应该不陌生它是一套把人跟安全绳、装备牢牢绑在一起的吊带。掉下去的时候真正救你的不是那根绳子而是绳子跟身体之间的这个连接结构——它保证力量能传递、动作能控制、危险能被卸掉。软件里的Harness架构做的也是同一件事把模型、数据源、工具、外部API这些容易失控的组件统一装进一个“安全吊带”里。它不是框架不强迫你用一套模板把代码写死它也不是平台不给你提供服务器和运行环境。它是一层薄薄的契约所有内部模块要暴露能力必须实现这个契约所有外部调用要进出系统也必须经过这个契约。任何组件崩了Harness能接住、能熔断、能降级任何一次调用都能被追踪token花了多少、在哪一步出错全部有据可查。那为什么不直接叫“中间件”或者“网关”因为中间件更像是“插在中间的一层”而Harness强调的是一种“约束关系”——不是帮你转发请求而是把所有东西绑在一条可控的边界上。这个边界就是安全网没有它一旦工作流里某个适配器悄悄改了返回格式或者某个模型开始输出乱码整个链路就散了。1.2 我做的这个应用到底解决什么问题准确说我做的是一款“个人AI工作流控制台”。它能干的活听起来不复杂把本地文件、RSS订阅、数据库、模型调用、定时任务、通知推送全部挂到统一的Harness下用一套DSL定义工作流。比如“每天早上八点抓取行业新闻调用模型生成摘要推送到IM群”或者“监控某个网页变化内容变了就用模型对比差异发一封邮件”。但一旦要支持几十个工作流并发执行、几十个数据源轮流接入、模型随时升降级、失败自动重试和降级“听起来不复杂”的事就变得极其复杂。如果没有Harness层这些功能最后一定会散落在各种独立脚本里每个脚本自己处理定时、重试、日志、token计费最后变成意大利面代码。有了Harness层每个工作流只是一个声明式配置真正的执行逻辑全部收敛到同一套调度器里。我只需要保证这一层是稳的上面挂再多东西都不怕。这个应用不是给普通用户用的它的目标用户是像我一样的“重度自动化爱好者”愿意用配置文件而不是按钮来控制一切希望所有AI调用能被审计、被配额、被观测的人。说实话市面上也有不少类似工具但我需要的是极致的可控性所以最终决定自己造。1.3 20万行代码都花在哪了很多人听到20万行就会觉得这是天文数字其实拆开看分布非常典型。我统计过项目里的代码占比核心Harness运行时大概占35%各类适配器——包括数据源适配器、模型适配器、输出端适配器——加起来占30%DSL解析器与校验器占15%前端控制台占10%剩下的测试与工具脚本占10%。这个分布能说明一个问题真正属于“骨架”的代码只有三分之一剩下的大头全在“跟外部世界打交道”。每接一个新的数据源就要处理它的认证、分页、限流、字段映射每接一个新的模型就要处理它的参数差异、流式输出、错误码、计费规则。一个人不可能凭手写快速覆盖这么多适配器但AI辅助下这种机械性的适配代码反而成了最容易批量生成的部分。20万行听起来吓人去掉适配器核心复杂度只属于“一个人可以掌控”的范畴。2. 一个人为什么敢写20万行代码2.1 20万行不是敲出来的是“review”出来的先算笔账九个月大概270天20万行分摊到每天是740行。纯手动敲键盘这个量基本不可能而且就算敲出来人也废了。我实际的工作流是先把模块的接口定义、依赖关系、边界条件写成清晰的文档然后基于文档让模型生成代码我来做代码审查、修并发问题、补异常分支、跑测试。每个月烧掉的40亿token大部分不是用来“写代码”的而是烧在“让模型理解上下文”上。比如我想改一个已经写了5000行的核心模块我可能要把这个模块的源码、相关测试、设计文档、最近几个issue的讨论全部丢给模型让它基于完整上下文给出重构方案。这一步的输入token就是几千甚至几万。再比如调试一个偶发bug我要把堆栈日志、相关数据样本、模型输出记录一起喂进去让它帮我猜根因。这种“上下文密集型”的用法token消耗远比生成代码要恐怖。所以“AI辅助编程”的正确姿势不是让AI从头写一个巨大文件而是把它当成一个记忆力极好、代码基本功扎实的结对搭档。你负责方向和决策它负责写初稿和找资料。10万行代码里可能有7万行初稿是模型写的但每一行都经过我的审查和修改。这个工作量依然很大但量级从“不可能”变成了“拼一拼可以”。2.2 用代码结构对抗“三个月后自己看不懂”一个人维护大型代码库最大的敌人不是写不出来而是三个月后回头看自己都不认识自己写的东西。我的解决方案是把Harness架构当成项目管理工具来用。具体做法是每个模块严格自包含。接口定义、实现、测试、文档、示例代码都放在同一个目录包内模块之间只允许依赖Harness核心层暴露的公开API禁止跨模块直接import内部实现。这个约束我写在CI脚本里违反就直接构建失败。这样做的效果是当我需要重构任何一个模块时只需要打开那一个目录不需要去全局搜索谁引用了我的内部类。这个约束看起来简单执行起来非常考验自制力。因为有的时候赶进度你会忍不住想“就这一次直接import一下省事”。但只要放纵一次一个模块的边界就被破坏后续所有模块都开始互相纠缠。到了第三个月已经没有办法安全地修改任何代码了。所以我在项目里专门留了一个工具脚本每次提交前自动检查模块依赖边界这可能是整个项目里最值得的一百行代码。2.3 九个月的时间线打怪升级式推进我的时间分配大概是这样的前两个月搭核心Harness运行时和DSL解析器这两个月是地基不能求快每天写得很少但要求每行都经过推敲第三到第五个月集中写适配器和前端控制台这段时期代码量增长最快因为适配器都是一套模式复制粘贴再修改很适合AI批量产出第六到第七个月做联调和压测主要发现一些分布式场景下的问题比如并发调度冲突、token配额失效、网络重试风暴第八到第九个月做文档、示例、发布准备。每个阶段我都设了硬性退出条件不满足不许进入下一阶段。比如“核心Harness必须通过故障注入测试”的意思是我会随机杀掉某个数据源连接观察工作流会不会卡死、能不能降级“适配器覆盖率”要求所有主流数据源必须能跑通端到端。没有这些闸门项目很容易陷入“前面偷工减料后面疯狂填坑”的恶性循环。3. 核心工程细节Harness层的设计与实现3.1 统一接入协议所有外部能力都是插拔的Harness层最核心的设计是三个抽象接口数据源Source、输出端Sink、模型端点ModelEndpoint。Source统一暴露connect()、poll()、parse()三个方法无论你连接的是数据库、RSS还是网页文件Sink统一暴露send()无论是发邮件、发IM消息还是写入数据库ModelEndpoint统一暴露complete()、stream()、cost()无论底层是哪个大模型。每个组件还必须上报自己的健康状态、最后成功时间、错误计数。这套设计的价值在于当我需要把某个模型从A换成B时不需要修改任何工作流定义只需要写一个新的ModelEndpoint适配器在配置里切换实现即可。数据源同理。整个Harness核心从来不需要知道某个具体适配器怎么实现它只要按统一契约去调用。这样适配器的增删不会影响核心稳定性核心的升级也不会破坏现有适配器。这就是可插拔的力量。你可能觉得这套接口设计平平无奇但真正关键的是错误处理也统一了。所有适配器抛出的异常都会被Harness捕获包装成带错误码、上下文、重试次数的统一错误对象。工作流可以针对不同错误码走不同策略超时走重试认证失败走刷新token限流走退避数据格式错误走告警。没有这个统一包装每个适配器自己写异常逻辑那就真的失控了。3.2 上下文管理与token配额控制烧出40亿的核心原因为什么一个月能烧出40亿token因为执行一个工作流时必须把任务描述、数据样本、历史结果、工具定义全部塞给模型。如果无脑灌一次调用就可能吃掉上万token几十个工作流跑一天几千万token就没了。我的对策是“分层上下文”。第一层是常驻指令只放最核心的1000 token描述整体角色和任务目标第二层是数据明细按需从数据源流式取不一次性全部注入第三层是历史记录用摘要折叠的方式压缩只保留最近5轮的关键节点第四层是工具定义只在模型需要调用工具时才注入相关schema。每一层都设置了预算上限超了就拒绝执行而不是静默截断。给每个工作流还设了单次执行token上限、每日总预算。一旦某个死循环或者异常逻辑开始疯狂调用它会先触达配额被Harness熔断而不是把当月额度烧光。这套配额系统大概花了三周时间实现但它是在第二个月下旬才做的前几周确实出现过一天烧掉几百万token的血泪教训。3.3 可观测性三件套再贵的token也要花得明明白白没有可观测性40亿token花哪去了根本说不清。因此Harness层对每一次模型调用、数据拉取、输出发送都统一输出七个字段组件名、操作类型、输入token、输出token、耗时、错误码、重试次数。汇总之后前端控制台实时展示每个工作流的花费排行。我在关键链路上还埋了“span id”一个工作流从触发到结束所有日志都带着同一个ID。一旦出错我可以把日志里所有相关记录串成一条完整链路看到底是哪个环节慢了、哪个调用返回了垃圾数据、哪个token在哪个环节消耗最多。这套系统帮我在后期排查了大量问题。举个例子有一次我发现某个工作流每天固定烧掉几百万token排查后发现是历史消息把每次的完整输出都追加到上下文里导致上下文越来越长。因为可观测性里能看到该工作流的输入token曲线持续上涨我很快定位到这个问题。如果没有这套数据这种问题可能会被我忽略很久。4. token烧了40亿钱花在哪怎么省4.1 40亿token的真实账本时间比token贵每个月40多亿token听起来很疯狂。折算成费用如果大部分调用高端模型按市场价大概是每月几万到十几万元人民币的规模如果混合使用平价模型会低不少但依然是普通人无法承受的成本。个人项目烧到这个量需要特别清醒地认识到一个逻辑时间其实比token贵得多。比如有一个模块我手写可能要一周模型辅助写加我来改可能只要一天中间烧掉几百万token。这一周的人力时间成本远远超过那几百万token的费用所以这个交换是划算的。反过来如果只是改一个README或者调一个CSS颜色也要开一个十几万token的对话那就是纯浪费。我的原则是只在“上下文复杂、需要快速产出初稿、需要快速理解陌生代码”的时候重度使用模型简单的机械操作绝不浪费token。这台账最后算下来40亿token换来的是一年之内完成了原本需要团队做两三年的工作量。4.2 省token三板斧缓存、批处理、上下文压缩第一板斧是缓存。相同输入、相同请求参数的调用结果写进Redis缓存命中直接返回。模型输出有很大的重复性尤其是摘要类任务、固定格式生成类任务缓存命中率能达到30%~40%。这部分省下来的token等于白赚的。第二板斧是批处理。很多场景不需要实时响应比如“批量审核十篇文章并打标签”完全可以并发拆成多个子任务然后在同一个上下文里合并处理或者直接用模型服务商提供的batch接口。实践下来批处理的token总量能比逐条调用省将近一半因为请求头、指令、公共上下文只需要传一次。第三板斧是上下文压缩。历史消息不要无限追加每次对话结束后先用模型把这一轮的重要结论压成300字摘要下一轮只带摘要和最新消息。这跟人脑遗忘曲线很像既保留了关键信息又控制住了上下文膨胀。这三板斧是我在第二个月做完的之后token消耗的速度才算是被压住了。4.3 别被“上下文越长越好”骗了4k能解决就绝不用32k长上下文是token黑洞也是效果陷阱。同样的任务4k token能解决的非塞进32k上下文成本直接翻几倍而且模型并不一定做得更好。上下文越长模型越容易在无关信息里迷失重点输出质量反而可能下降。我刚开始做的时候总是怕模型信息不足把一堆文档全部塞进去。后来发现很多场景下模型只需要几条关键规则和少量示例。所以后来我立了一个规矩任何工作流先写最小上下文能跑通再逐步加。每次加内容都要观察效果指标——如果效果没有实质提升立即回滚。这个原则帮我省下了大概20%~30%的token消耗也让很多任务的输出质量更稳定。记住给模型的“资料”不是越多越好而是越精准越好。5. 常见坑与排查实录5.1 token失效与续签的疑难杂症应用要调用大量外部APItoken管理是绕不开的坑。我几乎把所有经典报错都踩了一遍。“sign-in could not be completed token exchange failed: error sending request for url”这类错误九成是网络超时或者服务商临时故障重试时要带指数退避不要立刻原地重试“token endpoint returned 403 forbidden: country, region, or territory not supported”说明被地域限制拦截没有别的办法只能切换到合规的接入区域“failed to refresh token: 400 bad request: invalid refresh_token: empty string”这种多半是把过期access token又当成refresh_token传了过去或者存储时字段写串了。还有JWT续签的经典陷阱refresh token不要放在前端localStorage很容易被偷续签接口必须校验refresh token是否已撤销access token过期时间要设置一个合理的滑动窗口不能等到最后一秒才去刷新。这些坑我都记录在项目wiki里基本每一条都花费了我几个小时到一天的排查时间。对单体开发者来说这种“token玄学”问题最消耗意志力。5.2 长上下文丢失与格式错误的排查当工作流里需要模型处理长文档时最常见的问题是输出截断、JSON格式错误、字段串位。一开始我以为是模型不行后来排查下来发现往往是因为上下文里混入了旧版的输出示例或者输出schema没有做严格校验。对策有两个。第一所有模型输出先过一层schema校验器校验失败就自动重试一次同时把具体的失败原因——比如“第3行缺少required字段summary”——写回给模型让它修正。第二维护一个“输出示例库”每次生成时把最新、最成功的示例注入到上下文里而不是让模型凭印象输出。这两个改动落地以后低级的格式错误率下降了80%以上。在AI应用里严格要求输出格式与严格要求接口协议同样重要。5.3 一个人维护20万行代码的取舍九个月写出来不算本事后续继续维护才是真本事。我的经验第一条是学会删代码比写代码更重要。20万行里其实有一部分是“试错产物”一旦证明某条路走不通我会毫不犹豫地删掉整个模块而不是想着“以后可能还用得上”。留着僵尸代码的后果是每次全局搜索都会多出一堆干扰项心智负担越来越大。第二条是“单人重构规则”每次改动影响超过5个文件先停下来画依赖图评估是不是该拆模块了。一个人没有团队review必须用纪律代替监督。你可以给自己写一个checklist每次大改动之前强制核对接口是否兼容、测试是否覆盖、文档是否更新、依赖是否越界。这4条每次都能筛出一堆问题。还有一条心得是每天只允许自己花两个小时“扫尾”剩下时间必须用于业务功能推进。因为一个人太容易陷入无关紧要的完美主义比如调某个控制台的圆角或者给某个工具函数写花哨的装饰器。这些事放到最后再做核心链路才是决定项目生死的东西。如果非要给后来者一句建议我会说别把20万行代码当目标把它当结果。你在解决真实问题、持续迭代的过程中代码量自然会生长到这个体量。token烧多少也同理它不是KPI只是你换取时间和稳定性的成本。把注意力放在架构边界和可观测性上烧掉的token才能变成真正推进项目的力量。
阅读完成 · 觉得有帮助?
咨询建站