ARTICLE DETAIL

资讯详情

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

一个人扛下九个月:Harness架构如何让LLM任务系统从原型走向可控

一个人扛下九个月:Harness架构如何让LLM任务系统从原型走向可控 说实话我从来没想过一个项目会把我钉在电脑前面整整九个月。上个月API账单弹出来的时候我盯着那个数字看了很久——40亿token换算成主流模型的价格是普通人很难接受的月度开销代码仓库里躺着20万行代码提交记录里的作者字段只有我一个人而这一切背后的核心支撑是一个我自己摸索着搭起来的Harness架构应用。这篇内容不是教程也不是吹嘘记录而是一份复盘一个独立开发者为什么敢选Harness架构这种偏底层的路线又是怎么在九个月里把原型做成一个每天处理数万任务的系统。如果你正准备做一个重度依赖LLM的产品或者你已经在用现成的Agent框架但被token成本和失控的编排搞得头疼这篇复盘应该能给你一些参考。我会把架构选择的判断逻辑、九个月的时间线、20万行代码的构成、40亿token的去向以及我把token压下来的实操手段全部摊开来说。1. Harness架构到底是什么我为什么没有去套LangGraph1.1 先给Harness下个定义Harness这个词在中文社区里用得不算多你把它理解成驾驭层或者控制台/执行器分离就好。Harness架构的核心思想是把一个系统和它的控制逻辑彻底拆开控制系统Harness负责决定步骤、顺序、策略和异常处理执行系统只负责做具体动作比如调用模型、调API、写数据库。打个比方Harness就像交响乐指挥乐手不需要知道整首曲子怎么走只需要在指挥给出信号的时候演奏好自己的部分。在LLM应用里用户看到的往往是一个聊天框或者一个任务提交按钮但背后真正跑的是一个复杂流程先判断意图再决定要不要检索知识库然后调用模型生成内容中间还可能插入工具调用、人工审批、数据校验。每一步都会产生token消耗每一步都可能失败。Harness架构要解决的就是把这些模型调用和工具调用编排成一条可控的、可观测的、可重试的工作流。我在项目早期的理解很朴素Harness就是代码里的总导演。后来做到第五个月我才真正体会到它更深一层的作用——把状态和流程分离让每一个节点的输入输出可以被记录、被回放、被审计。这个认知差异决定了后来的架构走向。1.2 为什么一个独立开发者敢选这条路很多人听到一个人做架构选型第一反应是为什么不直接用现成的Agent框架省时省力社区维护。但我在做技术选型的时候考虑的不是demo好不好跑而是三个更现实的问题。第一系统一旦上线每一个模型调用都要能被解释清楚。现成的Agent框架在节点内部做了什么prompt拼接、选择了什么模型、为什么走上这个分支很多时候是一个黑盒。我可以接受闭源API做模型但不能接受编排逻辑也是黑盒。Harness架构的好处是整个流程的控制权都在自己手里任何一次任务异常都可以从日志里还原每一个节点的调用链。第二token成本取决于在哪一步调用了模型、传了什么进去。成本治理不是等账单出来才做而是在架构层面就决定哪些节点必须走模型、哪些节点可以用规则替代。现成框架把模型调用作为一等公民但我的场景里有大量不需要模型参与的分支判断自己写Harness可以把模型的使用率压到最低。第三也是最重要的一点大部分Agent框架是为对话型应用设计的而我的业务是任务型应用。对话需要的是多轮上下文管理任务需要的是状态机、并发控制、事件驱动和幂等重试。这两类场景的底层模型完全不同。用别人为对话设计的轮子去跑任务流水线后期一定会撞墙。当然自研Harness意味着所有坑都要自己趟。前两个月的代码几乎是在验证这条路走不走得通后面才逐步进入工程化阶段。1.3 和现成Agent框架的对比我并不是否定现成的Agent框架。它们在快速验证想法、做内部工具、处理相对简单的编排时效率确实很高。但拿它们和自研Harness对比后差距还是相当明显。维度现成Agent框架自研Harness上手速度快一天能跑通Demo慢前两个月都在试错流程控制粒度框架预设了节点/边模型灵活但有边界完全自定义任何行为都能实现可观测性依赖框架自身日志难以做到全链路埋点在代码层强制埋点每个节点天然可追踪成本治理默认接模型token消耗不可见可在调度层压缩、缓存、路由成本可控框架升级兼容跟随上游版本常有破坏性变更自己控制改坏了自己修适合场景原型验证、轻量编排重度任务系统、成本敏感的生产环境这张表其实也是我当时的决策依据。如果你的系统是给对话加个工具调用我认为现成框架完全够用。但如果你的系统是每天处理几万个复杂任务每一步都要审计、要控成本、要容忍失败重试那现成框架的通用设计反而会成为天花板。这就好比你要建一个专门送货的物流车队买几辆改装过的面包车当然能跑但如果你要的是全国调度网络最终还是得自己设计车辆和调度系统。2. 九个月走完的路线从周末原型到每天数万任务2.1 前三个月证明这条路能走通第一个月我做的事情很朴素用Python写了一个脚本把一个复杂的任务拆成几个阶段用if/else串起来每一步调一次模型。当时跑通一个端到端用例需要五分钟中间一旦某个环节返回格式不对整个流程就断掉。这个版本连Harness都算不上只是一个线性执行器。但这段笨拙的阶段极其重要因为它让我把所有隐藏需求暴露了出来。画流程图的时候我才发现我一个看似简单的任务实际状态竟然有十几个等待意图确认、检索中、工具调用中、等待外部系统回调、生成中、需要人工复核、失败重试中……线性脚本根本处理不了这种复杂度更不用说并发场景。第二到第三个月我开始认真研究两件事状态机怎么设计事件驱动怎么落地。我把整个任务抽象成一组状态每个状态对应一个处理器处理器做完自己的事情之后根据结果决定发布什么事件。这一版的关键产出不是代码而是一张被我反复修改的状态流转图。直到今天这张图还贴在工位墙上它比任何文档都更能说明Harness架构的骨架。2.2 第四到第六个月推翻重来向事件驱动靠拢第四个月我做了一个艰难的决定把前三个月的线性脚本全部推翻重写事件驱动版本。原因很简单线性脚本每增加一个分支复杂度就翻一倍而事件驱动架构可以横向扩展节点新增能力不需要改旧节点。重建的过程痛苦但也值得。我实现了两个核心组件事件总线和节点调度器。事件总线负责接收所有节点发出的事件节点调度器根据事件类型查表决定下一步调用哪个处理器。每个处理器只接收结构化的输入只输出结构化的结果不关心自己是被谁调起来的。这样一来整个流程的灵活动性和可维护性都大大提升。这三个月里的最大坑是事件风暴。事件之间互相触发偶尔会形成循环某个任务会在两个节点之间来回跳直到耗尽最大重试次数。排查了半天根因是调度规则没有做层级隔离。后来我引入了层级状态机的思路顶层是几个大的阶段每个阶段内部再管理自己的子状态跨阶段的事件优先于阶段内事件。这个改动让整个系统的行为一下子稳定下来。2.3 最后三个月可观测、审计和成本治理第六个月底系统已经能稳定跑通核心链路但我很清楚离真正常驻运行还差得远。第七个月我做了一次线上事故演练模拟了一个外部API长时间无响应的情况。结果惨不忍睹模型被循环调用token消耗暴增任务全部卡在重试队列里。这次演练直接催生了两个关键模块超时熔断和全链路token埋点。第八个月开始我把开发的优先级放在了可观测性上。每一个节点在调用模型之前都会记录输入token数量模型返回之后记录输出token数量节点结束时把这一段消耗挂到任务ID下。这样任何一个任务跑完都能看到一张完整的token消耗明细表。没有这一步后面40亿token的优化根本无从谈起。第九个月是上线冲刺月。我做了性能压测、回归测试集、监控告警又花了两周时间把一路积累的技术债清理了一遍。坦白说一个人到了上线前夜真的会有恐惧感——系统里的每一个代码分支都是自己写的一旦出问题没有任何人可以帮你背锅。但反过来正因为每一处都懂问题排查的速度反而比团队协作时更快。整个九个月的时间分配大概符合3-6-3原则三个月证明可行性三个月做工程化重构三个月做稳定性和成本治理。回头看如果压缩掉前三个月直接一步到位写事件驱动版本很可能会因为对业务状态理解不透彻而写出一个过度设计的架构。那段走弯路的时间其实是在为架构积累认知。3. 20万行代码的结构解剖每一块代码的用途和代价3.1 代码分布20万行代码这个数字听起来很夸张尤其出自一个人之手。但如果你拆开看会发现它不是某一个单体文件堆出来的而是分层架构的必然结果。我做了个粗略的行数统计虽然按文件行数算代码量不一定科学但从分布比例能看出系统重心在哪。模块行数约说明Harness核心引擎3万状态机、事件总线、节点调度、重试控制工具与适配器层5万对接外部系统、解析返回结果、统一数据格式上下文与记忆管理2.5万上下文压缩、滚动摘要、历史管理缓存与存储层2万语义缓存、结果缓存、任务持久化API与前端工作台3万任务提交、结果查看、人工复核界面测试与脚本3.5万回归用例、数据回放、模拟环境配置、迁移与其他1万配置解析、数据迁移、工具集成脚本核心引擎只有3万行说明Harness架构的精髓不在于代码多而在于把复杂度隔离在可替换的模块里。工具与适配器层占了5万行这其实是最容易被低估的部分。每接一个外部系统至少要写认证、请求、重试、错误解析、统一到内部数据结构这几层代码。一个外部系统大概要1500到2500行我前后接了二十多个这个量就起来了。测试与脚本的3.5万行在很多单兵项目里会被省掉。但我把测试当成第一道防线因为一旦系统跑起来每一次改动都可能是压垮骆驼的那根稻草。没有团队没有QA回归测试集是我敢改代码的唯一底气。3.2 哪些代码坚决自己写哪些代码绝不自己写一个人做技术选型最忌讳的就是什么轮子都想自己造。我的原则很直接凡是基础性、通用性、已经成熟的领域绝对用现成库凡是和业务编排、token成本、状态管理强相关的核心逻辑再难也自己写。绝不自己写的包括HTTP请求库、加解密、JSON序列化、ORM、前端UI框架、基础日志库。这些领域经过无数项目验证我再用一个月去重写一遍除了增加Bug概率没有任何收益。我会做的只是在外层包一层薄薄的适配统一调用入口。坚决自己写的是状态机核心、事件调度规则、缓存策略、模型路由器、上下文压缩策略。这些模块是Harness的灵魂也是控制token成本的关键位置。如果这些逻辑塞在某个框架的黑盒里我几乎没办法在模型返回错误格式时快速定位问题。自研的代价是很高的——状态机里一个细小的问题就可能导致事件死循环——但换来的是对每一块行为的绝对掌控。3.3 20万行里有多少是没想到的增量如果时间倒流到第一天我预计的代码量可能只有5万行。实际翻了四倍主要的膨胀源有三个。第一个是兼容层。不同模型服务商的返回格式千奇百怪有些直接给JSON有些给Markdown包着的JSON有些把结构化数据塞在自然语言里。为了让上层不用关心这些差异我写了大量解析和归一化代码。这一层至少贡献了3万行。第二个是埋点代码。做全链路审计意味着每个节点都有日志、指标、token计数。这些代码不产生业务功能但它的存在让系统从能跑变成敢跑。这部分大概占15%。第三个是防御性编程。面向外部API的异常你永远做最坏打算超时、限流、返回乱码、字段缺失、状态码骗人……每一类异常都对应一组兜底逻辑。这些代码平时不执行但一旦触发能救回一整条任务。真实项目里的代码量大部分是给那5%的异常情况准备的。4. 40亿token的去向一个完整任务的token账单4.1 先算一笔账40亿token一个月摊到每天是1.3亿多token。按我当时用的主流中端模型价格粗估如果输入输出比例按3比1估算一个月的成本在两三万美元级别再考虑部分调用走了轻量模型和缓存命中实际支出大概在十几万到二十万人民币之间。这个数字对个人开发者来说绝对是压力它逼着我把token当成系统的一等公民来看待。很多人听到40亿token会觉得夸张但你要知道任务型系统不像聊天工具那样一次只消耗几千token。一个复杂任务里的多次模型调用会把同一个上下文反复拼进去。比如一次要读十份文档并生成报告的任务原始文档是2万token但因为每一步都要把相关摘要和中间结果重新组装实际消耗可能是原始内容的5倍到10倍。Token的放大效应在任务型场景里非常明显。4.2 一个典型任务的token明细我拿一个实际场景举例一个用户提交了分析最近一周的用户反馈找出前三大问题并给出建议的任务。这个任务看起来简单但拆开看背后的模型调用链是这样的步骤调用内容输入token约输出token约意图解析判断任务类型、提取参数150080数据检索调用搜索/数据库输出结果30000工具调用结果解析判断是否找到足够数据3500120数据分析生成初步分析结论12000800报告生成根据分析结论写最终报告200002500格式校验检查报告格式并修正4000300一次任务总计约4.5万token其中输出只占很小一部分大头几乎全是输入。这就是LLM应用的特点你花钱买的主要不是模型的思考而是阅读和理解。40亿token除以单任务4.5万大约对应一个月几万个任务考虑到还有很多更轻量的子任务整体的数量级是吻合的。4.3 看不见的三个token黑洞拆解完典型任务后我又做了全链路日志分析发现实际消耗比预期多出很多多出来的部分来自三个隐蔽的黑洞。第一个是重试导致的重复调用。外部API超时一次任务会重试每次重试都要重新调用模型读取上下文。尤其当超时发生在一个已经消耗了2万token的节点一次重试就是翻倍的成本。后来我加了缓存每次模型调用的输入做一个hash如果完全一样直接复用上次输出而不是重新请求。第二个是工具返回的原始内容没有压缩就塞进上下文。工具返回一个JSON数组可能有几千条记录其中真正有意义的字段可能只占20%。既然模型只关心结论就不应该把原始JSON整段喂给它。我在工具适配层加了一个提炼步骤先把工具结果变成精简结构再进入模型上下文。第三个是长会话历史不做摘要。早期版本为了让模型保持上下文连续会把之前所有轮次完整拼接进去。会话一长输入token像滚雪球一样膨胀。这个问题直接催生了后面要讲的滚动摘要机制。5. 让token从40亿降到25亿的实操三板斧5.1 第一板斧语义缓存Token优化不能靠拍脑袋每一步都要看两个指标成本下降了多少任务成功率有没有下降。我做的第一个大动作是语义缓存。最基础的缓存是精确缓存同样的输入hash直接命中。但任务型系统里完全相同的请求很少。更多的情况是两个任务的目标相近、参数不同这时候精确缓存就失效了。于是我做了语义缓存把任务的核心意图和关键参数提取出来用向量表示新任务进来时先在历史任务里找相似度超过阈值的记录如果命中直接复用当时的输出结果。这套机制落地后整体缓存命中率从0提升到30%上下。需要注意不是所有节点都适合缓存。像意图解析这种前置分类节点因为输入变化频繁缓存意义不大但像数据检索、分析结论生成这类结果相对稳定的节点缓存收益很高。我的经验是分节点设计缓存策略而不是做全局一键缓存。5.2 第二板斧上下文压缩与滚动摘要这是token消耗下降最明显的一板斧。上下文压缩分两层。第一层是工具结果的压缩前面提过对外部返回的长文本做提炼只保留结构化关键字段。第二层是会话历史的滚动摘要当任务经过多个节点后不再把每一轮原始对话全部保留而是每隔几步就把历史记录压缩成一段摘要新的模型调用只看摘要 当前节点输入而不看完整历史。有人担心摘要会丢信息这个担心是对的。所以我的方案不是无脑丢弃而是把原始历史存到存储层摘要只服务于当前模型调用。如果下游节点需要追溯到某个细节可以通过工具调用从存储层重新取出来。这样既控制输入token又不牺牲信息完整性。这套机制让单任务平均token下降了大约20%。5.3 第三板斧模型分层与路由最后一个大动作是把模型按能力分级让轻量模型干轻活。任务的第一步一般是意图识别和参数提取这类任务对推理能力要求不高用一个便宜的轻量模型就够。真正需要强推理的节点比如数据分析、报告生成才走最强模型。我还写了一个简单路由器根据节点类型、输入长度、任务复杂度三个维度决定走哪个模型。规则核心很简单如果任务需要读长文档并理解因果关系走强模型如果只是做分类、抽取结构化字段、判断格式走轻量模型。这个改动带来的收益是综合性的不只是降本还让推理速度变快了。因为大量轻量节点的响应时间从十几秒降到了两三秒整个任务的完成时间大幅缩短。三板斧做完每月token从40亿降到了25亿下降幅度接近40%而任务成功率不降反升因为摘要和缓存减少了上下文干扰。6. 一个人维护这套系统的生存底线6.1 测试是唯一的安全网单兵作战最怕的是改一个模块另外三个模块悄悄坏掉。九个月里我改过至少五次核心状态机如果没有回归测试每一次重构都等于自杀。我的做法是维护一个黄金用例集几百条历史任务覆盖各种正常路径和异常分支。每次改动后不需要把所有用例全部跑完——那样太慢——而是先跑快速集再抽跑完整集。更重要的是我把线上的真实任务做了数据回放把历史任务的输入和当时的外部API响应录制下来重放给新版Harness跑对比最终结果和期望结果。这种回放式测试比手写mock要真实得多因为它用的都是真实world的数据。6.2 把成本做成实时可观测指标成本治理不是月底看账单而应该是实时的。我把token消耗做成了四个核心指标每任务token中位数、节点级token消耗分布、异常任务最高成本、缓存命中率。其中最有用的指标是每任务token中位数和异常任务最高成本。前者反映整体成本趋势只要它上升说明有某个改动让上下文变膨胀了后者负责抓黑马一个任务如果token消耗远高于中位数通常意味着有人踩了重试黑洞或者工具返回了超长内容。我设置了告警规则一旦发现某类任务的token消耗连续三次超过历史均值两倍就自动触发熔断禁止继续跑新任务。这样能避免一个坏配置让整个月的预算在半天内烧光。6.3 踩过的几个坑最后分享几个让我印象深刻的坑。第一个是事件成环第九个月时一次配置错误导致两个节点互相触发任务在两者之间循环了四十多次才被重试上限掐断。解决方式是引入层级状态机并且给每个事件加了来源标记禁止同一级的循环触发。第二个是缓存污染。语义缓存命中了一个相似但语义上并不可比的旧任务导致新任务返回了过时答案。后来我加了一个条件只有当任务参数中所有关键字段都匹配并且意图向量相似度超过0.92时才允许缓存命中否则走真实调用。第三个是摘要压缩时丢字段。有一次我把历史压缩成摘要后下游节点需要的关键字段被摘要掉了导致整段生成逻辑崩溃。解决方式是在压缩器里定义必须保留字段列表任何摘要都不能删除这些字段。第四个是模型升级带崩解析器。模型服务商更新版本后输出格式在边缘case上发生了变化我的解析器直接抛异常。这件事之后我写了一个格式漂移检测定时用少量探测请求检查模型输出格式一旦发现和预期结构不符立即告警。一个人维护系统靠的不只是代码能力更是预判意外的能力。如果让我重新选一次我还是会选Harness架构。不是因为自研有多酷而是它在最难的时候给了我掌控感——每一个节点的输入、输出、成本、异常我都能随时翻开来看。九个月、20万行代码、每个月几十亿token这些数字都不值得羡慕真正让我踏实的是这个系统即使只有我一个人维护也敢在深夜上线一个重构版本因为我知道每一行代码背后的边界在哪里。
返回列表