NO SINGLE SOURCE事实没有单一来源
提示词和输出模板一边示范「全包」,一边禁止「全包」。问价判定散在至少 9 处,「是不是个微」的判据 7 处以上,没有一处说清该用哪一份。
「全包」修了两次还在发。病根不在某条规则,而在链路结构:一次生成,再层层补救。
// 给默默,抄送野荞、Gus · 代码基线 origin/main 61d2216c(2026-10-10)· 之后的代码变化见第 13 章(截至 165e2f5e,2026-10-14)· 标 待验证 的是推断或没实跑的
同类问题反复出现,是因为链路先让模型自由生成,再靠一层层规则去补。补救层越多,新错越多。
提示词和输出模板一边示范「全包」,一边禁止「全包」。问价判定散在至少 9 处,「是不是个微」的判据 7 处以上,没有一处说清该用哪一份。
报价、面积换算写在提示词里让模型算,事后正则兜底。原稿的「4.5 万」就是算错的:89÷2×568 ≈ 2.5 万。
违规后同一个模型盲重写,看不到原稿;个微重写温度从 0.35 升到 0.7。发出版868、300 两个锚点没了,冒出没有依据的「选中能享补贴价」,疑与闸的升级词有关 推断。10-11 起(#2072)企微、个微只重写 1 次,第二次的升级词不会再用上;盲重写和 0.7 的温度还在。
原先全包替换只在 chatReply 内部做,route 追加的话、确定性快答、投递闸兜底句、已审话术直发都不再查用词。#2069 起这些出口都接了同一个 applyBannedWording,但词表里只有「全包」,没有统一口径源。
不换框架、不改成 agent。在现有 TS 里拆成五层:L1 理解L2 事实L3 写话L4 统一出口L5 决定,再配单一口径源 policy 和回归语料网。「全包」作为第一个样本走 Phase 0,每一步都有 KV 开关,可影子运行,出问题秒级回滚。
光点就是一条客户消息。点任意节点,下面会展开这一步出了什么问题、怎么修。切到「改后」看五层链路。
口径只写一次。提示词、价目块、重写纠正词、出口检查、测试断言全部从它派生。Langfuse 只存语气和策略,不再存事实口径。
每修一个事故,加一行「事故句 → 期望输出」。补上渠道 × 人设矩阵(含 wx_* 托管人设)。改前改后各跑一次,只许更好、不许更差。
同一轮里模型写了三版(Langfuse trace 14ae62b4…)。本该只纠正一个数字:编面积闸连续两次触发重写,终审又漏掉个微,最后发出了第三版。
图片占位被改写后闸认不出,带面积的原稿被当成「编造面积」,触发盲重写。纠正方向从一开始就偏了。#2085 已修:闸改读画像里的 sent_image。
「2.5 万」再次命中编面积闸,第二次重写带上只许留 568 这种单价,868 和 300 被删。推断#2072 起企微、个微没有第二次重写,这条路径已不存在。
终审替换的判据只认企微,个微请求三项全假,函数直接 return,「全包」原样发出。#2069 已修。
// 「设计安装全包」这串字在 src 里一处都没有。它来自提示词示范,还是门店资料 c83ee422(正文有「设计安装售后全包」)待验证
「全包」是一步步被写进代码、被测试保护起来的。两次修复都只修了症状,没删源头。
// 口径:GitHub 全量 2064 个 PR,按标题和正文匹配全包 / W-85 / W-86,加 git log -G'全包' 的 83 个提交,逐个读 diff
近一个月改到回复大脑的 fix 提交有 100 条。按标题关键词粗分,同一套结构问题在各个口径上反复出现。
// 「渠道/人设/提示词」33 条是前三类都不命中的 44 条里,再用更宽关键词命中的部分。换一组近义词,各类会浮动几条
口径散在提示词、价目块、模板、门店资料、测试期望里
policy.ts 单一源 + 源码守卫,L4 用同一份做终审
模型在文本里自己算,确定性报价只在一个分支生效
L2 FactPack 由代码算好,L3 用占位符回填
问价 9+ 处、城市解析器 8 个 只做了 grep、个微判据 7+ 处
L1 TurnFacts 具名字段,全仓只判一次,双跑对账后下线旧的
channel === "weixin" 有 46 处 165e2f5e;61d2216c 为 45;一个 weixin 同时代表 Winds、安卓、macOS 三端
渠道和人设进 TurnFacts,渠道策略默认从严
盲重写 + 升级词 + 多层删句,最后没有一致性复核
L5 只 reask 1 次否则转人工,L4 检查残句
已部分收敛:门店删句 09-30 起只在一处拼,登记表 10-07 落地
门店资料进 FactPack,登记表补 textPolicy 列,不重复做
步骤固定,模型只出现在「写话」和「reask」两处。这是 workflow 的写法,不是 agent。把鼠标移到右侧图例上,对应的层会浮起。
{{unit_price}} 占位回填。| 机制 | 类别 | 出处 | 适用边界 |
|---|---|---|---|
| 理解 → 事实 → 生成 分层 | 经典架构·类比 | Gao 等《Neural Approaches to Conversational AI》§1.2;Jurafsky & Martin 附录 K | 经典架构的策略在生成前;本方案 L5 在写话后,是事后校验 |
| LLM 只出判定,回复用模板 | 开源项目 | Rasa CALM:LLM 只建议下一步,回复默认是预定义模板 | Flow Policy 是确定性状态机只在搜索摘要里看到 |
| reask:带原稿和原因重生成 | 开源项目 | Guardrails AI on_fail=REASK,源码把原输出作为 previous_response 传入 | 带原稿这点要看源码确认 |
| 最终输出统一校验 | 开源 / 厂商文档 | OpenAI Agents SDK output guardrail;NVIDIA NeMo output rails | NeMo 允许改写,本方案选择不盲改写 |
| 不盲改写,给外部反馈重来一次 | 论文·单篇 | Huang 等,ICLR 2024(arXiv 2310.01798) | 测的是推理基准;现状重写已带违规原因,论文不能直接证明现状错 |
| 数字由代码算 | 论文 | PAL(arXiv 2211.10435);Toolformer(arXiv 2302.04761) | 只核了 arXiv 摘要页 |
| 单一口径源 | 行业标准 | 《The Pragmatic Programmer》DRY | — |
| CI 强制架构约束 | 行业标准 | ThoughtWorks fitness function;ArchUnit / Import Linter | 原书未打开,以 Radar 为准 |
| 新规则对每个出口表态 | 本方案组合 | 穷尽性检查 + fitness function + DRY 的组合 | 没找到现成先例,按组合创新对待 |
| 影子运行 + KV 开关 | 行业标准 · 仓内已有 | Fowler Strangler Fig;Istio 流量镜像;Feature Toggles;仓内 android-planner-shadow.ts | Strangler Fig 用在提示词流水线上是类比 |
| 回归语料进 CI | 开源 · 仓内已有 | promptfoo、OpenAI Evals;仓内 eval-fixtures | 沿用仓内 harness,不引入 promptfoo |
覆盖 10-10 那条消息走的主回复出口。方案写成时召回、opener、kf 不经过出门闸,要到 P2/P3 才补齐,这里不说成「全部修死」。净增约 40 行 估计。当晚 22:39 的 #2069 已先把下面步骤 1–4、6 和步骤 8 的一部分做完(做法与下面略有不同),召回、opener、kf 也接上了禁用词闸;各步现状见第 13 章。
|| channel === "weixin",保留 isSalesPersona。只改用词,不改报价数字。已做(#2069,用 isPrivateOneToOneChannel)· src/lib/llm.ts:3548-3553P0 要等第 12 章第 1、4 件拍板,放行当天可上线。沿用 10-06 审计结论:随修随拆,不单独开一轮重构。开关放 system_settings KV,缺键即 shadow,{"mode":"off"} 立即停用。
第 07 章那 10 步。
验收:测试先红;线上 24h 柜体「全包」= 0 · 主体已由 #2069 上线,24h 取数待验证全包、全国统一价、补贴口径收进来,提示词由它生成。
验收:守卫测试先在旧代码跑红登记表加 textPolicy 列,所有出口只观察不拦截。(#2069 已直接以 bannedWording 一列、拦截方式上线,只含「全包」。)
验收:违规率 · 每个出口有表态顺序:全包 → 门店 → 价格。
回滚:KV 开关秒级关闭旧判定照用,分歧写 turnFactsDiff,为 0 后逐个切换。
切换条件:分歧为 0先接个微问价这一条路径。
约束:ydyx golden 逐字不变北极星:实发 ≠ 原稿的比例下降。(企微、个微已缩成 1 次纠错,见第 13 章。)
盯:modelDraftChanged · 转人工率放最后:改一句措辞,曾 4 次采样回归连伤两轮。
配 eval 双跑以后加任何一条不许说 X / 必须说 Y,都要同时满足这五条。能交给 CI 的交给 CI,不靠人记。
绿色是新建文件,圆点是在该阶段改动,横条表示跨多个阶段持续改。
// llm.ts 现 5627 行、signals.ts 3100 行、route.ts 6686 行(61d2216c;165e2f5e 为 5682 / 3117 / 6713)。行数下降后两道尺寸闸都要收紧(check-file-size / check-size-ratchet)
模板、报价块、reroll 两个渠道共用;kf 只传 persona 也走同一套。企微 reask 温度 0.7,与现在持平。
预算 75s、路由上限 95s(Winds 端上只等 75s,服务端对 salespc-* 主机收紧到 55s,#2077)。企微、个微的重写已在 #2072 从 2 次减到 1 次,误拦的代价因此更高:只要一道闸误拦、那 1 次重写又没过,这条就不发出。10-13 就有客户说「40方」被编面积闸误判(#2124 补了正则)。
L3 改一句话就可能伤回归,所以放最后,并配 eval 双跑。
quoteProfile 分支的报价必须逐字不变,只有 P0 的用词替换例外。
Python local_fastpath 和 macOS sanitize 在服务端闸之外,登记表只能标 exempt 或 gap。
改 fallback 必同步 Langfuse;高危路径以 high-risk-paths.regex 为准;评测用 EVAL key;样本不带真手机号或 wxid。
每件都写了默认做法:不回复就按默认执行。第 1、4 件决定 P0 能否开工。
本方案按 61d2216c 写成。之后进 main 的 81 个提交里,动到回复链路的主要是下面这些。有的把方案里的事提前做了,做法不完全一样;有的让前面某句话不再成立。除标了待验证的最后一行,其余都对着代码读过。
| PR | 做了什么 | 对应方案哪一项 | 现状 |
|---|---|---|---|
| #2069 10-10 22:39 | 新增 outbound/banned-wording.ts:出口统一改写「全包」,喂给模型前把上下文里的「全包」中性化。终审判据补上个微,改成按句处理,装修语境叫「整装」。已审话术直发、opener、召回、kf、route 出门后都接上。exit-registry 加第 5 道闸 bannedWording,19 个出口逐个表态。企微、个微 nurture 规则和价目块里的「安装全包」示范删掉(抖音 DM 提示词里的仍在),golden 重录。没做:policy.ts、decision / opener 提示词的禁令、eval 放开 wx_* 人设、quality-watch 标记。 | F3 F9 F10 F11;F12 只覆盖一个词;第 07 章步骤 1–4、6;第 09 章闸 3 | 部分落地 |
| #2072 10-11 | 新增 reply-contract.ts、reply-state.ts、order-quote.ts。企微、个微的重写、推理泄漏重试、终审纠错共用 1 次额度,重写循环只剩 1 次;防复读并进终审;终审不过就不发、本轮转人工。客户拒绝过的和指定配色记成会话状态;订单报价由代码算好、进提示词、出门前核对;发送前再复检有没有新消息、报价变没变。事后对算式句整句放行的分支(COMPUTED_TOTAL_RE)删除。没做:原稿不带进纠错,重写温度仍是 0.7(终审那次纠错 0.4);L2 只做了订单报价这一块,没有占位符回填。 | F5 F6 F7;第 06 章 L5;P6;第 11 章风险卡 2 | 部分落地 |
| #2085 | 客户发过图记成画像字段 sent_image,编面积闸只读它,IMAGE_RE 删除。 | F1;第 03 章闸 ①;L1「只判一次」的第一个实例 | 已修 |
| #2124 #2127 #2134 | 「40方」「四十方」也认作报了面积:8 处客户面积正则一次补齐,之后两次修中文数字的误判。#2127 把重写失败的原因分成「额度用完」和「请求异常」。 | 第 05 章「意图判定」:同一个判定写了 8 处;风险卡 2:误拦会让整条不发 | 相关,结构没改 |
| #2075 #2081 #2082 | 价闸误判让整条没发,各补一处:群发里的「原价868」不再当成已报价;城市不明却说「没有实体店」改成问城市;见光板 200 / 240 等官方价不再被表外单价闸拦下。 | F2 F4:闸和口径各写各的,每次事故补一处 | 相关 |
| #2072 #2088 | 498 档问「什么牌子」改成确定性回答:#2072 先做了厦门版(xiamen498Brand),#2088 改名 tier498Brand 并推到全国(reply-contract.ts:28、fast-keyword-replies.ts:277);#2072 还在 getSystemPrompt 里按正则把与客户选择冲突的决策规则过滤掉(llm.ts:2527-2530)。 | F4:同一条口径现在写在库里的规则、代码里的过滤、固定答案三处 | 相关 |
| #2107 #2114 | 报价发出时落一行 quote_issued,/api/status 多一项 quote_issued_7d;weixin_reply_withheld 的 client 按主机前缀映射。 | 第 08 章验收指标,可加一项;只观测,不拦截 | 观测 |
| #2077 | Winds 主机(salespc-)服务端出话天花板收紧到 55s。 | 第 11 章风险卡 2 里的预算 75s、路由上限 95s | 原文要改 |
| #2118 | docs/audit-20261006 追记 §7(10-13):S1 已做,护栏表现为 5 道闸;「全包」连修三次,上线后效果没取数;「硬事实改由代码填槽」本轮不下结论。 | 第 08 章沿用 10-06 审计结论的那几条;P5、P7 要不要做仍待核 | 参考 |
| 其余 | 个微客户表(#2084 #2087)、已签 / 同行不代回(#2094 #2097)、通话记录不代回(#2080)、召回侧(#2106 #2110 #2113 #2115 #2128)、公开原句接口(#2143)、Winds / 安卓客户端修复。只看了改动文件清单和提交说明,没读全部 diff 待验证 | 不改回复大脑的结构;召回渲染仍走 bannedWording(render.ts:119) | 不影响方案 |
F1、F9、F10 已修;F11 在「全包」一个词上已修;F3 的示范已删,禁令仍只有一条;F7 在企微、个微上不会再触发。F2、F4、F8 的结构没动,F5、F6 只动了一部分,F13、F14 我没看到改动。五层里只有 L5 的重写 1 次、不过就转人工落了地,L1、L2 只有零星实例(sent_image、订单报价),L3 和 policy.ts 没动,turn-facts.ts / fact-pack.ts / placeholders.ts 都没新建;#2069 的做法是给「全包」单独造一道出口闸,没有走方案里的单一口径源。
// 行号和数字以 165e2f5e 为准;第 02 章链路图里标 61d2216c 的行号与本章冲突时,以本章为准