多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Harness架构:一个人如何构建可控AI应用的工程实践

Harness架构:一个人如何构建可控AI应用的工程实践 九个月20万行代码每月烧掉40亿 token。这三个数字放在一个只有我一个开发者的项目上确实有点疯。去年这个时候我做了一个决定——不再买一堆“AI工具拼盘”凑合着用而是动手做一个真正从底层归自己管的应用。所谓“真正归自己管”不是把开源项目部署一下换个皮肤而是从架构设计到每一行业务代码全部亲手落地。今天这篇不是项目炫耀是想把这三个数字背后的工程决策、结构性思路和踩坑过程完整讲一遍。核心话题叫Harness架构这不是论文里定义过的词汇而是我在这套系统跑通高强度的生产环境后觉得比任何现有叫法都贴切的描述。如果你正准备用大模型做正经应用或者你是独立开发者、小团队里的“光杆工程师”想把一个中型AI项目扛下来这篇文章应该有点参考价值。先别被题目里的数字吓到咱们一件一件拆。1. Harness架构到底是什么我给模型套了一副缰绳“Harness”这词英文原意是马具、安全带也指测试里的“测试脚手架”。我借这个词核心想表达两层意思约束和承载。约束是把模型的自由发挥限制在业务边界内不让它跑偏承载是把记忆、工具、权限、持久化这些工程能力全部挂在模型外面而不是塞进模型内部。说白了模型是发动机Harness是变速箱、悬挂、底盘和仪表盘。发动机确实是最重要的核心部件但真正能开着上路的是整辆车。这个比喻我跟很多朋友讲过几乎所有人都秒懂。1.1 两个设计直觉比名词更重要第一直觉模型永远是“会出错的组件”。它不像数据库事务有ACID保证不像函数调用有确定性的返回。它每次都给你概率性的输出温度一高还带随机性。所以架构的第一任务是“把不确定性关在笼子里”——模型只负责推理其他一切需要确定性的事情都由模型外面的代码来兜底。第二直觉一切都要可观测、可量化。我每个月烧40亿 token这里面每一笔都要能回答三个问题谁调用、为什么调用、花得值不值。如果模型躲在框架的层层封装里你连一次失败的调用都追踪不到那这种抽象再华丽也不能用。Harness架构从一开始就把“可观测性”当成一等公民这不是加分项是底线。1.2 五层模型我最终落地的Harness长这样整个架构最终收敛成五层每层管一件事边界清晰到哪怕三个月不碰代码回来也知道去哪改Bug第一层是模型接入层面向所有底层模型API做统一封装。不管背后是文本模型、代码模型还是多模态模型对外都暴露同一个调用接口负责路由、重试、模型切换和成本记账。这一层相当于整个应用的“南向接口”。第二层是任务编排层负责把用户的一句话拆成可执行的步骤。比如用户说“帮我把这份数据变成可视化报告”编排层要决定先调哪个分析工具、再让模型总结、最后渲染成图。编排层只做决策不做具体执行。第三层是工具执行层管所有外部能力的注册与调用。数据分析、网页抓取、代码执行、文档读写——每一个能力都注册成一个标准化的工具函数模型通过工具名和参数去调用执行结果再回传给编排层。第四层是记忆与状态层管会话上下文、长期记忆、向量索引和全部状态持久化。模型本身不保留任何东西每一次对话的上下文都是这一层组装好再喂给模型的。这层的设计直接决定token消耗量级。第五层是接口安全层管用户认证、鉴权、限流和审计。这里涉及token体系后面我会专门讲。五层之间通过明确的数据契约通信比如编排层输出的“步骤清单”是严格的结构化数据工具执行层返回的“执行结果”也必须带状态码和元信息。这样每一层都能独立测试、独立回滚不会因为一个小改动牵一发动全身。2. 九个月20万行代码一个人怎么不被自己写死20万行代码是什么概念按一个工作日写800行高质量代码算9个月大约200个工作日满打满算16万行。实际上你不可能天天满负荷产出还要查文档、调Bug、改设计。所以20万行意味着平均下来我每天的有效产出超过1000行而且是在保证可维护性的前提下。这背后没有秘诀只有近乎刻板的工程纪律。2.1 代码分解先算清楚20万行分给谁开工第三周我做过一次粗略的模块行数测算后来实际完成情况跟这个预测高度吻合说明事先的规划不是摆设模块预计行数实际行数主要职责模型接入层1.5万1.8万API封装、模型路由、重试、成本记账任务编排层3万3.6万任务解析、步骤编排、分支决策工具执行层4万4.8万内置工具开发、三方工具适配、执行沙箱记忆状态层2.5万2.8万上下文组装、摘要压缩、向量检索接口安全层1万1.2万认证、鉴权、限流、审计日志前端与交互4万3.5万Web界面、实时任务流展示测试与脚本3万2.6万单测、集成测试、回归脚本、数据构造系统集成与部署1万1.2万配置、CI、监控、日志采集这里最反直觉的经验是测试代码不能省。很多人一个人干活就放飞自我不写测试结果第三天就在重构里把自己写的代码改崩了。我是把测试当“第二份代码”来写的比例严格控制在七比三左右。20万行里有接近3万行是测试和脚本它们看起来拖慢进度实际上帮我保住了后面几个月不返工。2.2 一个人也要有的四条纪律第一条按领域分包不按技术层分包。很多新人喜欢按“控制器层、服务层、数据层”来组织代码那是教科书写法。我一个人做全栈按这个逻辑分改一个需求要横跨三个目录、改五六个文件。我的做法是按业务领域分chat包管对话编排tools包管工具执行memory包管记忆状态。每个包内部再分内部模块。这样改一个业务点时95%的改动都集中在一个包内。第二条类型全覆盖从第一行就做到最后一行。我用的是Python但坚持每个函数都必须带完整的类型标注和可用的数据模型。回应参数、工具返回、任务状态全部定义成显式的数据类。这么做的直接收益是IDE补全和重构提示变得极其准确你想犯类型错误都难。一个人写代码最怕什么最怕写完自己都看不懂参数是啥。类型标注是花钱最少、收益最高的保险。第三条小步提交一天至少几个commit。我给自己定的规矩是每次只改一个逻辑点改完立刻跑单测跑完就提交。有人觉得一个人仓库无所谓干完一个里程碑再提交也不迟。但如果你连续四天不提交第五天改出问题回滚都不知道滚到哪一天。高频提交配合清晰的提交信息相当于给自己存了无数个存档点。第四条不引入“搞不懂原理”的依赖。作为独立开发你没人帮你查一个第三方库的源码。凡是核心路径用到的依赖我都强制自己至少读一遍关键部分的源码。读不懂的就换一个实现宁可自己写个简化版。这个决策帮我规避了好几次上游库放弃维护的坑。2.3 一人团队的CI和自动化一个人最缺的就是“有另一双眼睛帮你检查”。所以我用CI脚本补上了这个角色。每次推送代码自动跑三件事全量单测、关键链路的集成测试、还有静态检查和代码风格校验。我坦白讲有一段时间我对CI响应的速度比对女朋友消息还快——推送后90秒内必须知道结果红了就立刻修。这一套流程跑下来我最大的体会是一个人写20万行代码真正难的不是“写出来”而是“还有勇气继续改”。如果代码裂成一团乱麻第九个月你是绝对提不起精神再动它的。3. 每月40亿token烧在哪成本治理比写代码更难40亿token按通用定价粗略算哪怕按百万token一美元的综合成本一个月也是几千美元起的真实投入。9个月下来光token成本就够买一辆车。所以“每个月烧掉40亿 token”不是一个抽象的规模感是真金白银在逼我优化。我把自己上个月的实际token消耗做了个拆解结果挺吓人的。3.1 token消耗的五大黑洞第一个黑洞多轮对话的重复计费。用户每发一句新消息系统要把完整的系统提示词、历史对话、检索到的资料、工具返回结果全部重新发给模型一次。一次两次看不出问题但当单会话累计几百轮对话时每轮新提问都在为一个越滚越大的“历史包袱”付费。这部分占到我总消耗的大头一度超过50%。第二个黑洞工具返回结果被反复打包。我的工具执行层有时返回一份完整的分析报告可能几万字符。这些内容作为上下文传给模型时按token计费远比想象中贵。尤其是排错类工具返回的错误堆栈动辄几千行模型每思考一步都要看一遍。第三个黑洞模型自我纠错和重试。模型输出质量不稳定时我做过自动重试和“让模型自我反思后再答一次”的设计。这种“再想一次”听着智能实际是双倍甚至三倍的token消耗而且收益并不稳定。第四个黑洞评测和回归测试。我要保证架构变化不引入模型行为回退所以得跑大量回归用例。每个月花在这种“模型测试”上的token高峰时超过总消耗的15%。这属于质量成本没法完全砍掉但必须控制。第五个黑洞日志和调试。生产环境里每一条关键调用链路我都会打印详细的输入输出。模型调用这种动辄几千token的日志一旦量大了本身就在产生存储成本和潜在的重复解析成本。后来我把模型日志降级为可配置开关默认只记录元信息需要排查时才打开完整日志。3.2 三板斧裁剪、复用、缓存第一板斧是激进的提示词压缩。我有一份系统提示词早期写得太饱满足足有3000多token包含各种示例和格式说明。后来我一步步压缩到900token以内方式是把长段说明改成紧凑的结构化描述把示例从提示词里挪到了“按需加载”的动态模板里。这件事收益极大因为系统提示词是每一次请求都要带的砍掉2000多token相当于全站请求的固定开销普降六成以上。第二板斧是上下文摘要化。与其把完整历史对话一股脑塞给模型不如在会话进行中不断把早期对话压成摘要只保留最近几轮完整内容和所有关键结论。我专门写了一个“上下文管家”模块实时监测当前上下文的token用量一旦超过阈值就触发摘要压缩。这个机制把长会话的平均上下文体积压缩了约65%而且质量损失用专门设计的保留策略控制——关键结论、量化数据、用户明确偏好这三类信息是强制保留的。第三板斧是三层缓存体系。精确命中型的请求级缓存、语义近似型的向量缓存、以及结果发现缓存。像“总结这份文档”这类高频操作如果输入是同一个文档完全没必要每次重新调用模型生成。我的缓存命中率长期维持在20%左右虽然不高但省下的都是纯利润。说个实际数字经过这三板斧我同一个核心工作流从最初每任务平均消耗约80万token硬生生降到了16万左右整整降了五倍。这就是为什么标题里说“每月40亿token”一般人会以为是失控烧钱实际上是在高负载下反复压缩之后的结果。3.3 token预算的月度分配逻辑音频、视频、文本、代码各类型模型价格差异很大。我设置了一个“每月token预算表”按用途分配额度核心业务推理占45%工具链往返和编排占25%缓存和检索占15%质量评测占10%剩余作为应急缓冲。每个月月末我会拉一次“token账单”按调用方维度统计真实消耗发现超预算的模块立刻开会——给自己开也行——第二天就出优化方案。4. Harness核心实现细节模型接入、上下文、工具与认证前面讲的是设计思路和钱的问题这节进入代码层面。我会把Harness里最关键的几个实现点展开讲每个都是我反复调过无数轮的。4.1 模型接入层统一抽象但绝不包死模型接入层初看就是一个通用封套把OpenAI格式、Anthropic格式、国产模型格式全部适配成同一个调用接口。但有个细节容易被忽略不同模型的“性格”差异是真实存在的它不只是接口格式不同而是输出格式偏好、指令遵循能力、推理深度都不一样。所以我没有把降级路由做成简单的“出错就切换”而是维护了一张“模型能力画像”表记录每个模型在不同类型任务上的历史表现。比如代码生成任务优先用模型A长文档总结用模型B意图识别用轻量模型C。路由策略平时按画像表走只有达到熔断阈值才触发降级。这一层还必须做三件事统一重试策略指数退避加抖动、统一超时控制不同任务分配不同的超时预算、统一成本记账每次调用都在本地记录模型名、token数、耗时。这三件事看着不起眼但长跑下来是保命级别的。4.2 上下文管理组装器模式上下文组装器是Harness里最核心的组件之一。它做的事情是把系统提示词、相关记忆片段、最近对话轮次、工具调用结果拼装成一次模型调用的最终上下文。听起来简单实际上每个环节都有讲究。系统提示词我设计成“基础模板动态注入”的形式。固定的世界观和行为准则写在基础模板里凡是跟具体任务相关的说明都推迟到任务编排阶段动态注入。这样同一个模型既能处理客服场景也能处理数据报告不会互相污染。对话历史的组装策略是最近的3到5轮完整保留再往前的轮次用“摘要关键信息”方式表示。摘要不是简单让模型“总结一下”而是有专门的结构化模板——必须保留决策点、已知条件、未解决问题、用户原始偏好。这些字段对后续任务执行有直接价值。检索记忆的插入时机也很讲究。不是每轮都塞一堆向量搜索结果那是对token的浪费。只有当前任务确实需要外部资料时才把检索结果作为一个独立的“参考资料”区块插入。插入位置放在历史对话之后、模型开始推理之前。4.3 工具注册与执行让模型安全地“动手”工具执行层是我写得最谨慎的部分因为这是模型从“纯粹说话”到“动手操作”跨越的地方。每个工具注册时都要声明工具名称、参数JSON Schema、权限等级、是否可用状态、执行超时时间和预算上限。模型只能拿到工具清单和参数约束拿不到任意代码执行的能力。工具返回结果的回传格式也做了严格约定。每个工具返回都带三个字段执行状态成功/失败/部分成功、规范性数据按声明格式返回、补充说明人类可读的上下文信息。模型看着像自然语言实际上数据结构是强约束的方便编排层做逻辑判断。这里有个我踩过很深的思想工具返回结果不能直接原样塞给模型。一个数据库查询可能返回几千行数据全塞给模型不但贵还会让模型迷失在细节。我写了一个“结果精简层”根据任务的当前意图把返回结果转成摘要、统计信息或关键行样本再决定是否附带完整数据供模型需要时追问。4.4 认证token的生命周期管理接口安全层涉及认证体系这里单讲token管理因为我见过太多应用在token处理上出问题。我用的是无状态JWT做访问凭证配套Redis做会话状态管理。JWT本身只认签名不存状态天然适合水平扩展但痛点在于签发后难以吊销。我的方案是短周期访问token加中周期刷新token访问token有效期设为15分钟刷新token有效期7天。每次刷新时校验用户会话状态如果用户在后台改变了权限或者被封禁刷新接口直接返回拒绝。这个机制避免了一个常见问题——“logout之后旧token还能用”。因为访问token短命最坏情况15分钟后自动失效而刷新token强依赖服务端会话状态登出即删除。还要强调一个细节token续签时的竞态处理。前端并发多个请求可能同时触发刷新导致多个刷新请求同时打到后端。我的处理方案是加一个“刷新锁”同一个会话同时只允许一个刷新请求真正执行其他请求等待或用旧token重试。这个问题在并发量上来之后特别容易踩。5. 九个月里最典型的五个故障与排查实录挑五个对架构有深远影响的故障每一个都让我改进了Harness设计。这些问题单看都不起眼但放在高强度使用场景下分分钟变成关键路径上的“绊倒大象的蚂蚁”。5.1 上下文污染模型越聊越傻的病根现象是同一个任务会话初期模型表现很好聊到几十轮之后明显变笨——格式开始混乱重要指令开始遗忘。最初怀疑是模型幻觉后来通过日志定位发现根因在上下文组装器早期某轮里用户一句“你可以随便点”被模型当成了“不需要遵守输出格式”的指令后续每轮都带着这个“误解”在推理。这类问题有两个典型来源一是对话历史里用户的口语化表达被模型误解成了元指令二是工具返回里夹带的“干扰信息”改变了模型行为。我的解法是三层防御第一对用户消息做系统性去歧义检测到歧义表达时用框定语言澄清再入历史第二工具返回必须走结构化封装剥离掉可能被误读的文本第三系统提示词里加一行“只有明确出现在系统指示中的要求才是规则”把历史对话降级为参考信息。5.2 模型调用失败后的雪崩重试某个周末上游模型服务出现大面积延迟我的统一重试策略是按指数退避重试3次。结果几千个用户任务同时触发重试攒在一起变成对上游的二次流量攻击反而拖垮了更多请求。那晚的故障复盘让我重新理解了“退避”的含义——退避不是给自己喘息时间是给上游恢复的时间。最终方案是“三层退避阶梯”瞬时错误最多重试1次间隙0.5秒超时错误重试2次间隙线性增长上游明确返回限流时进入熔断状态30秒内直接返降级结果。降级结果也不是硬失败而是返回“暂时无法处理请稍后重试”不让用户看到光秃秃的错误码。5.3 “模型自己在骗自己”工具返回错误被当成数据有一次工具执行层返回一个查询超时错误本来应该进入重试或报错分支。但在一次新版本中我把错误文本直接拼进了上下文模型看到这段文本后竟然接着这个“错误内容”往下推理仿佛拿到了真实数据产出了一份完全错误的分析报告。这是最危险的一类问题不是模型幻觉是模型把系统返回提示当成了事实输入。修复方案工具执行层的所有输出必须带明确的状态标记成功和失败用完全不同的数据通道进入上下文。失败时模型看到的不是错误详情而是一句标准话术“工具调用失败原因已记录建议换一种方式完成该步骤”。把“事实”和“状态”彻底分离。5.4 单机内存与并发一个人也要讲究弹性我的应用跑在一台云主机上内存有限。有段时间每逢高峰Python进程的内存占用飙升最终触发OOM。定位后发现两个元凶一是对话历史对象在内存里无限累积即使已经被摘要压缩原对象没有及时释放二是并发任务缓存没有设置上限无界增长的缓存吃光了内存。一次性做了四件事引入内存中的上下文对象池淘汰不活跃会话给所有缓存加LRU上限关键路径改为异步批处理避免同时挂起太多模型调用加上内存攻防监控指标超过阈值主动降载。从此OOM成了历史。5.5 刷新token过期引发的“登录死循环”前面说认证token设计时我提到刷新token但有一次因为刷新token策略和前端实现没配合好出现了一个诡异Bug用户明明token还有效但前端因为时钟偏差判定过期触发了刷新刷新后新token又被后端立即拒绝表现为“登录死循环”。排查到最后发现是刷新锁的粒度问题刷新接口要求旧token必须还是活跃状态的但前端的“刷新锁”提前把旧token标记成“已使用”后端的并发刷新请求全部拿着已吊销的旧token来换新token。修复方式是给刷新token增加版本号只有最新版本才能换取下一轮令牌旧版本即使未过期也直接拒绝。这个细节看着小处理不好就是全线登录异常。6. 五件事我现在会做、但九个月前没想到的回头看有五个判断我现在无比坚定但在项目开始时完全没有意识到。写出来是希望后来者能少走弯路。第一件事架构的“硬约束”必须早于“花哨功能”落地。我早期花了很多时间做漂亮的交互效果结果模型输出不可控交互越漂亮越尴尬。如果重来我会先花两周把上下文管理、工具权限、可观测性这三大基础设施钉死再碰任何业务功能。第二件事token耗尽是可以用架构解决的不是只能靠“少用”。很多人一谈降本就想到换廉价模型其实真正的成本大头往往在上下文管理和工程链路设计上。同样的模型Harness设计得好坏成本差距可以到五倍以上。第三件事AI应用里的“错误处理”和传统软件完全是两码事。传统软件的错误是确定性的要么异常要么返回码AI应用里最大的“错误”是沉默的错误——模型用漂亮话包装一个完全错误的结论。所以我在Harness里加了“结论验证”环节凡是关键任务的输出都会用独立通道做一致性检查。第四件事一个人开发情绪管理比技术管理更重要。九个月这么长中间必然有连续一两周觉得自己做的东西是垃圾的时候。我给自己定的缓解办法是建立“已完成里程碑清单”每完成一个阶段就把成果和最初版本截图对照用可见的进步对抗“原地踏步”的错觉。第五件事数据是你的才是真的。第三方框架和托管服务确实省事但数据和审计日志一旦沉淀在别人的平台上你就失去了做深度优化的依据。我自己维护全部日志和token账单虽然初期麻烦但我能做每一次成本优化和故障分析全凭这些数据底子。这九个月最大的收获不是产品上线也不是代码量本身而是我彻底相信了一件事模型再聪明也只是系统里的一个组件把一个组件调教好、约束好、安放在合适的位置上是工程问题而这个工程问题的答案就是Harness。往后我做的任何一个AI应用都会从这个框架出发不在它不擅长的领域逞强也不在它擅长的领域退让。
返回列表