ARTICLE DETAIL

资讯详情

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

事故出在模型外面:从 Prompt Engineering 走向 Harness Engineering 的四次线上复盘

事故出在模型外面:从 Prompt Engineering 走向 Harness Engineering 的四次线上复盘 过去一年我们把大模型接进一款跑了多年的某电商接待机器人前端入口是微信客服会话。接完之后线上出的事故复盘下来没有一起的根因在模型里模型本身没有变笨价格没变上游也没换过几次。出事的全是包在它外面的那一层——喂给它的教材、下一轮带进去的上下文、往外传输的流式协议、以及放它执行动作之前的那道闸。这一层有个英文名字Harness直译是挽具让一匹马愿意拉车的所有马具、缰绳和车辕。我给它记的账是系统风险里模型自身占两成上下Harness 占八成——这个数不是什么统计结论是把下面几起事故按所在层归一次类之后的结果。每修好一起系统行为的变化量都超过我们调过的任何一次模型参数。所以这篇不聊提示词怎么写得更巧聊的是真正拦住事故的那几层工程喂什么、怎么传、怎么兜底。一、先把模型外面那一层的边界画出来大模型一旦进真实产品系统就变成一条链教材语料与知识库 → 上下文组装 → 模型推理 → 流式输出 → 客户端渲染外加执行动作之前的意图闸、事实闸、幂等与缓存。模型只占链条中段那一格而且那一格有两个好性质它无状态不会记得什么坏事它可替换上游永远有备选。好消息是能力退化容易回滚坏消息是——事故基本不发生在它身上发生在你喂它什么、它说出去的话怎么传、传出去之前有没有人把关。把这几层摊开对应到我们这一年的事故清单上事故现象一句话所在层根因一句话修复动作教材放大器回复带表情比例 46%提示词压不住喂什么·语料60 条样例里 50 条自带表情模型照样例学样例表情清零 按会话比例放行上下文污染复读同一段话反复回越滚越长喂什么·上下文检索命中的答案被当成客户说的话写回上下文只存双方原始消息意图闸误伤71 秒内自动发了 4 次报价单怎么兜底·闸关键词匹配命中对面 AI 话术的字面关键词判意图整体废弃规则只守事实闸流式碎片化打字机被打碎客户端每秒重绘十几次怎么传·协议上游不合规范的小帧被原样透传网关重建 chunk 120ms 微窗口合并改语料不生效教材洗完线上行为纹丝不动喂什么·缓存版本元数据没 bump缓存键没变改语料必须 bump 版本版本由指纹生成五行一年全中。“所在层这一列就是 Harness 的分界喂什么语料、上下文、缓存怎么传流式协议、边界怎么兜底意图闸、事实闸、幂等。模型层不在这张表里——不是说它没出过格式不遵守、偶尔瞎编这类毛病是那些最后都在边界层被兜掉了值得单独立案的事故一件也没有。下面按现象→排查弯路→真根因→修法→教训”把前四起挨个复盘第五起短一些但它是前一起的续集。二、事故一教材放大器——提示词压不住的示范在压你现象RAG 知识问答上线一段时间后运营做话术回查发现回复带表情符号成了一个显著问题抽样统计带表情的回复比例到了 46%。电商接待的场景里客户正为退款催得冒火收到一个眨眼的表情投诉概率是要涨的。注意这不是模型答错了内容是答对了内容、用错了语气——内容层面的抽检一切正常恰恰说明这类问题不在答对答错的监控视野里。排查弯路第一反应当然是提示词。加不要使用表情符号不行比例降了一点又回去加得更重“禁止输出任何表情符号包括文字版表情”还不行再往后把常见表情列成负面清单逐条点名来回十几版提示词比例卡在 30% 以上下不来有时不降反升。期间也动过把温度压到接近零、换上游模型的念头都被另一件事按住了换模型一次要跑一轮全量回归成本摆在那而46%这个数字实在不像采样波动能解释的量级。有同事在准备动手换模型之前提了一句要不再看看发给模型的消息里除了指令还有什么真根因那一轮请求里除了系统提示词还带着与知识库配套使用的对话样例——也就是教材。60 条样例数了一下50 条的回复里自带表情。于是模型同时收到两种信号指令说不许用五十条示范说用了很正常。指令管显式规则示范管行为倾向两者冲突时倾向赢。46% 差不多就是 50/60 这个比例被提示词的那点抑制力往下拽了拽之后的结果。提示词不是没用是它的杠杆长度压不过教材的示范效应——你怎么改措辞都改不了多数样例都这么回这个事实。修法两步都不在提示词里做。第一步洗教材样例里的表情全部清零业务上确实需要某种语气的个别样例除外——清点下来几乎没有一条真的需要。第二步把允许不允许带表情从提示词里的禁令改成一个按会话维度裁决的开关以近期窗口统计大约三成会话允许带表情其余会话一律纯文本这个开关在网关侧实现进请求之前就把指令和教材一起摆平。修完之后监控里表情比例回落到可以接受的区间并且可接受的区间本身成了一个可配置项而不是靠提示词措辞去碰运气。教训语料质量闸的优先级高于提示词。模型行为倾向出问题第一个该看的不是自己写了什么指令而是它正在照着什么示范学。这个顺序定下来能省掉十几轮提示词往返。后来我们把这条写进了排查手册的第一行先查教材再查提示词最后才轮到模型。三、事故二上下文污染——自己的回答被当成客户的话再喂回去现象部分会话偶发复读客户问了一件事机器人回了一段下一轮又把上一段几乎原样回一遍。有时是两句相邻回复高度重复有时越滚越长像卡住了直到人工介入才停。排查弯路这个现象有个教科书式的名词退化重复。于是先按教科书排查——调重复惩罚考虑升温怀疑采样把模型推进了循环。又怀疑是检索在捣乱某一条知识被反复命中所以反复输出拉出检索日志核对命中条目其实是有变化的对不上。绕了几天真正转向是从一个具体动作开始的把某一出问题会话实际发给模型的 messages 原样打出来看。一眼看到一条客户消息内容是机器人自己上一轮的完整回复。真根因上下文组装环节里知识库命中的答案被以客户说的话的身份写回了会话上下文。模型下一轮看到的是一条客户提出的问题而这条问题恰是自己的上一段回答——它的合理行为就是再答一遍或者就着这段话再确认一遍。污染条目不会自己消失它留在上下文里每一轮都被重新带进请求像滚雪球复读要等到窗口把它顶出去才自然停止。模型没有病是我们给它喂了一份假账。修法上下文写回加一道守卫只允许原始消息进门// context-guard.js —— 上下文只允许两种身份进门// role: customer | agent —— 谁说的// source: raw | generated | retrieved | fallback —— 从哪来的functionappendTurn(ctx,msg){if(msg.source!raw){// 模型回复、检索摘要、兜底话术一律不许伪装成任何一个角色进入上下文。// 上一轮我说过什么确实要带给模型只允许以 roleagent 的原始发送记录进入// 让模型知道这是我说的而不是客户说的。metrics.inc(context_append_rejected,{source:msg.source});returnctx;}if(msg.role!customermsg.role!agent)returnctx;return[...ctx,{role:msg.role,content:msg.text,ts:msg.ts}];}规矩两条上下文里只存对话双方的原始消息——客户说的记成客户我们发出去的记成我们任何生成内容和检索内容不得以客户身份进入上下文。这里有个取舍值得说清楚上一轮机器人说过什么本来就需要让模型知道否则多轮接不上。但正确做法是让它以agent 原始发送记录的身份进上下文而不是伪造一条客户消息。复读的成因有第二条腿——模型分不清喂进去的内容是不是自己说的——但这条腿不该靠模型去分辨该在写回那一刻就不让上下文变脏。教训上下文是数据管道不是日志桶。每条消息的出处谁说的、从哪来必须是写回时的必查字段。出处错误比噪声危险噪声只让模型这一轮说错话出处错误会让模型把假话当事实继续推理而且每一轮都带着它。四、事故三关键词意图闸——对面一句客气话让我发了四次报价单现象产品里有一条自动流程当判断客户在要报价单时接待机器人直接发送报价文件。某天这条流程失控对同一个会话在 71 秒内自动发了 4 次报价单。客户一头雾水发来一句怎么发这么多遍。排查弯路第一反应是节流没生效翻网关配置节流参数正常第二反应是模型异常触发了发送调用调出四次触发的来源记录发现四次全部由一个确定性的关键词闸触发跟模型没有半点关系。这次事故里模型压根不在场。真根因“客户是否要报价单这个判断当年是用关键词匹配写的消息里出现报价单”“发报价之类的字样就判定成立放行发送。问题在于那个会话的对面接的也是一家 AI 客服。对面的话术里有一句您发报价单过来我帮您看看”——“发报价单三个字字面命中。两边都是模型一边的客套话成了另一边的执行指令。关键词闸不知道这句话是谁说的不知道语义上是我要一份还是你把你的发我一份”它只看得见字面命中命中之后发送动作又没有会话级幂等于是闸每被碰一次报价单就发一次71 秒四次。修法当天的拍板结论很硬关键词判意图整体废弃。意图判断交给模型——它看得到整句话、说话的人、语气和上下文分得清给我来一份报价单和您发报价单过来我帮您看看的差别关键词规则永远写不出这个判别式。但确定性规则不是全拆而是换岗只守事实闸维度事实闸保留确定性代码意图闸废弃关键词交给模型判断对象状态报价文件是否存在、该客户是否可见该价目、本会话是否已发送过语义客户是不是在要报价单输入结构化数据取值可枚举自然语言整句加上下文失效模式数据缺失误判可审计、可回放字面串误伤语言面穷举不完兜底手段发送前校验 会话级幂等键动作侧节流、当日发送上限、异常告警重建后的流程是一串与运算模型判意图成立且文件存在且该客户可见且本会话未发送过四个条件全真才执行发送发送动作带会话级幂等键那 71 秒里的第四次会被动作侧直接拦掉。闸还在而且比原来更硬只是它从此不读文本猜心思只读状态对事实。教训确定性规则只能守事实闸不能守意图闸。意图是语言事实是状态拿规则在语言面上匹配意图早晚被语言淹——尤其对面也接了模型之后AI 话术里那些高频字面串恰好是关键词表最躲不开的。还有一条附赠的节流和幂等必须放在动作侧。放在触发侧等于要求所有可能的触发者自觉而触发者里现在多了一个会客套话的模型。五、事故四流式碎片化——上游打碎的字不该到客户端再补现象接待客户端用流式方式展示模型的思考过程与回复像打字机。某段时间开始打字机变得磕磕绊绊思考内容一行一个字往外蹦客户端每秒重绘十几次界面视觉上是碎的。排查弯路先在客户端动手给重绘加节流、改脏矩形、上虚拟滚动做完卡顿缓解了一点但碎没消失——因为文本本身进来就是碎的渲染层只是收到了什么画什么。又怀疑上游模型吐字速度变了抓了对比结论模棱两可。真正的转折是有人把上游原始响应帧直接抓了下来网关在透传而透传出来的帧本身就不合规范——一行一个字的小帧、部分帧缺协议必带字段、finish_reason这个字段有的上游传的是空字符串按协议应该是 null 或者枚举值空串是第三种东西两边解释可以完全不同。客户端收到一堆不合协议的伪帧只能照单全收。真根因透传。网关把传输做成了中继上游发什么帧就原样转什么帧把上游的帧行为当成了自己对外契约的一部分。上游是第三方网关它的帧规范在我方控制之外而它显然没有严格守流式协议。协议不合规范这件事一旦发生透传就会免费扩散到每一个下游模块。修法不透传。网关不再把上游帧原样下发而是把它解析成增量文本按我方与客户端约定好的流式协议重新建帧加微窗口合并# 网关侧流式重建伪码吸收上游碎帧吐出自家合规流 buffer # 待下发的增量文本 window_start None # 微窗口起点 seq 0 # 自有序号与上游帧号彻底脱钩 on 上游增量到达(delta): # 上游帧再碎止于此处 if window_start is None: window_start now() buffer delta.text # 只取增量文本不碰上游帧结构 if now() - window_start 120ms or buffer 到达句子边界: 下发合规chunk(seq 1, content buffer, finish null) buffer ; window_start None on 上游连接结束: # 不信上游的 finish_reason 字段 # 结束信号以自己的语义为准收到显式终止标记或读超时 等微窗口到期把剩余 buffer 作为最后一个正文 chunk 下发 下发收尾 chunk(finish stop) # 枚举值一律由网关生成窗口取 120ms是在重绘频率降到个位数每秒和体感延迟不可接受之间量出来的一个折中上游再疯到我这就变成最多每 120ms 一帧的稳流客户端只面对自家协议不面对上游的脾气。finish_reason由网关统一生成不再有空串外泄。效果是问题从结构上消灭了——不是给每种坏帧写一个补丁而是坏帧根本进不了下游视野。教训边界层的协议要在我方收敛而不是转发。上游不合规范这件事该在我的边界上被结构性地终结在客户端和每个下游模块里逐帧打防御补丁是同一个学费交 N 遍。对上游协议的防御是网关的职责不是 UI 的职责。这条不限于流式任何把字段语义由上游决定当省事设计的透传都在埋伏笔。六、续集改语料不是存个文件是一次发布事故一洗完教材我们紧接着撞了一个小但经典的坑表情清零发布监控里表情比例纹丝不动线上行为像什么都没发生。当时第一反应是洗教材没用示范效应没那么强差点把已经做对的修法推翻。真根因在模型和语料之外上下文组装侧有缓存缓存键里带语料的版本标识。那次改动只替换了内容文件版本元数据没 bump——键没变缓存永不失效线上读到的还是旧教材。改了没生效看起来像模型问题其实是缓存问题而它之所以能骗过我们是因为现象和提示词压不住教材太像了。修法是一条死规矩改语料必须带版本变更且版本标识由内容指纹自动生成不靠人记得手改。语料的进出走发布流程——审、版本、灰度、观察和代码一个待遇。教训和事故一同层合起来是一句话教材语料是系统行为的依赖项凡是依赖项改动就是发布发布就得让所有缓存和副本知道。七、Harness 检查清单全文压成十条按事故代价从大到小排。每条都对应上面某一次交过的学费没有一条是凭审美加的。模型行为倾向出问题语气、冗长度、格式偏好先查教材语料再查提示词最后才轮到怀疑模型。提示词指令与教材示范不许对着干对着干时示范赢。上下文写回必须过守卫只允许双方原始消息进门生成、检索、兜底内容一律不得以客户身份进上下文。意图判断交给模型确定性规则只守事实闸——存在性、可见性、是否已执行过。节流与幂等放在动作侧不依赖触发侧自觉触发者里可能站着一个会客套话的模型。上游流式帧不透传解析成增量文本后按自家协议重建 chunk用微窗口合并摊平碎帧。协议级枚举字段如结束原因由我方边界生成上游的空串、缺字段不许扩散到下游。语料与知识变更必须 bump 版本版本由内容指纹生成缓存键里没版本等于没有缓存失效机制。监控要看行为比例表情率、复读率、误触发率、重绘频率光看错误率和延迟这几起事故一起都不会报警。每次事故复盘先定位所在层喂什么、怎么传、怎么兜底。不在模型层的事故不要用换模型去治。八、几句收尾两成和八成的账不是贬低模型。恰恰相反模型能力每往前一年那两成还在继续变小但 Harness 的八成不会跟着模型升级消失它只会换个地方重新长出来——教材换一批污染换个姿势协议换一版上游碎帧换个花样。马越壮挽具越不能凑合因为力气大了挣脱之后跑得更远。这一年四起事故加一个续集教会我们一件事别指望模型更自觉要让它外面的那一层没有不自觉的空间。喂进去的每一条消息有出处传出去的每一帧有归属执行前的每一个动作有闸。做到这三件模型就只是链条里那个可以随意替换、也很少背锅的零件——而零件很少背锅这个状态本身就是 Harness 工程的全部意义。
返回列表