
说明本文讨论的是同一个 AI 能力接入多个产品入口时答案不一致的来源与治理办法属于工程架构话题不涉及具体模型版本与价格。AI 领域版本迭代极快凡涉及版本号、价格、可用性请以你阅读时的官方页面为准。文中代码为结构示意请按自己的技术栈调整后再上生产。一、不一致是怎么长出来的先把入口清单落到纸上收到「网页端和 App 答得不一样」这类反馈时最不该做的一件事就是直奔提示词去改。先做的是把入口清单写下来。一个能力铺到多个入口落点通常是这几处网页对话页、移动 App、对外开放 API、IM 群里的机器人、客服工作台。它们看起来是同一个能力背后却是四五套独立的技术栈、四五拨不同的维护人。清单至少要写四列入口、技术栈、状态形态、维护人。少了「状态形态」这一列第五章和第六章的问题根本无从谈起——因为状态形态决定了这个入口会不会共享会话、要不要共享缓存。入口技术栈状态形态常见偏差网页对话页前端直连后端服务有会话上下文命中会话级缓存移动 App客户端 → 网关 → 服务有会话上下文版本发布滞后开放 API三方系统直调无状态单次请求调用方自带提示IM 群机器人消息回调 → 服务以单条消息为单位无跨消息上下文客服工作台内部系统内嵌有工单上下文检索过滤条件不同四处来源逐一对照把不一致的来源穷举下来其实固定是四处不多不少来源具体表现典型触发版本不同入口调到不同的模型版本灰度发布、分批升级提示每个入口各写一份 system prompt各团队分别迭代检索索引版本不同、过滤条件不同知识库分批重建缓存有的入口命中缓存、有的没有键里带不带入口标识这四处的特点是每一处单看都「没问题」合起来就出错。版本差异只要有一个入口没跟上灰度两边就分叉提示各写各的某个团队顺手加了一句「回答要简洁」输出风格立刻不同检索那边索引重建没切齐同一个问题召回的资料就不是一批缓存最隐蔽命中与不命中看起来都正常。四处叠加之后不一致几乎是必然假设四个变量各自有八成的概率在两个入口上一致四处独立叠加两边完全一致的概率不到一半。这个估算很糙但它想说明的事很清楚多入口不一致不是偶发故障是默认状态。不做任何收敛动作两个入口给出不同答案才是常态一致才是碰巧。举个具体到不能再具体的例子网页端提示里加了「回答尽量引用原文」App 端没加。同一个知识型问题网页端会附三行出处App 端给一句结论。两边都没坏可用户看到的是两个系统——他甚至会怀疑其中一个在编。所以治理的起点不是「修 bug」而是承认这套系统本来就会分叉然后逐个变量把它按回同一口径。二、把四个变量拆开变量不拆开归因永远是猜最常见的归因方式是看到两边答案不同就说是「模型不稳定」。这句话没有信息量因为你无法据此做任何改动。四个变量不拆到可独立观测的粒度任何「我觉得是提示的问题」都只是猜测。拆的办法是把每次回答的关键上下文都打点出来四个变量各留一个字段。⚠️ 代码待验证# 每次回答都把四个变量打点缺一个就没法归因defrecord_answer(entry:str,question:str,answer:str,ctx:dict)-dict:return{entry:entry,# 入口标识归因的主键model_version:ctx[model_version],# 变量一版本prompt_id:ctx[prompt_id],# 变量二提示用 id 指纹不存全文prompt_hash:ctx[prompt_hash],# 提示指纹两边比对看是否同源index_version:ctx[index_version],# 变量三检索索引版本retrieval_filter:ctx[filter],# 检索过滤条件必须显式cache_hit:ctx[cache_hit],# 变量四是否命中缓存cache_key:ctx.get(cache_key),# 命中时要能反查键怎么拼的}这段里最容易被漏掉的是cache_key。缓存命中不一致时只有拿到键才能知道两个入口为什么一个命中、一个没命中。prompt_hash而不是提示全文是为了在日志里既能比对同源、又不把提示原文到处散落。一次只动一个拿到打点之后归因的原则是一次只动一个变量。同时改版本和提示然后发现答案一致了你并不知道是哪个改对了下次还会分叉。可控的复现方式是固定其他三个只让一个变量在两个入口上分叉看答案是否跟着变要验证的变量固定住只放开判据版本提示、索引、缓存两个入口用同一提示、不同版本答案是否随版本变提示版本、索引、缓存两个入口用不同提示、同版本答案是否随提示变检索版本、提示、缓存两个入口用不同索引版本召回片段是否不同缓存版本、提示、索引只让一个入口关缓存关缓存后是否回到一致归因要留一份可复查的记录一次归因做完结论只有「是检索的锅」这句话是不够的。要留下一份能复查的记录问题、两个入口的四个变量取值、两边答案、以及比对的结论。这份记录后面会直接变成第七章对照跑的问题集来源。记录里字段的顺序建议固定成入口在前这样按入口扫不一致率时可以一眼看过去不用每次重新理解表结构。归因记录和线上打点用同一套字段名能省掉一层转换——转换的地方越多越容易在关键时刻对不上。三、提示与配置的收敛共享底座 入口差异化片段多入口的提示不能各写一份但也不能强行合成一份——IM 群里的机器人需要更短的输出客服工作台需要引用知识源这些差异是真实需求硬压平反而会让两头都不好用。可行的结构是共享底座 差异化片段底座负责身份、安全边界、输出格式这类所有入口都必须一致的约束差异化片段只放本入口特有的东西。底座改动要走统一评审片段可以由各团队自行迭代。⚠️ 代码待验证# 提示的组装结构底座唯一片段按入口声明prompt:base:id:base_v3# 底座版本号所有入口必须引用同一个hash:b41f...# 底座内容指纹发版时比对entries:web:fragment:web_short# 只放输出更短这类本入口差异allow_override:[tone,length]api:fragment:api_strict# 严格遵循调用方传入的格式约束allow_override:[format]im_bot:fragment:im_group# 群聊场景 与转义处理不同allow_override:[length]差异化允许改什么、不允许改什么这是收敛能不能守住的关键。差异化片段必须有白名单不能「各入口想改什么改什么」。维度是否允许入口差异理由身份与安全边界不允许一旦某入口放宽安全口径就破了输出长度与语气允许不同载体天然需要不同长度输出格式结构允许但要显式声明API 入口常需要严格结构是否引用知识源允许客服工作台必须引用群聊可省拒答与兜底策略不允许属于安全边界的一部分工具调用权限不允许与第四章的过滤条件联动白名单外的东西一旦出现在某个入口的片段里应该在配置加载时就报错而不是等上线后才发现两个入口口径不同。报错的时机越早代价越小。配置要登记、要能 diff底座和片段都必须是可 diff 的文件不能散落在代码里、数据库里和某个人的剪贴板里。每次发布前跑一次 diff用两列告诉评审人这次改了哪几个入口的哪一段。底座改动的影响面是所有入口所以它的评审应该比单个片段更严。反过来片段变更只需要确认没有碰到白名单外的维度。这里有一个容易踩的坑有人为了图快把本该放底座的约束塞进某一个片段的allow_override里——白名单校验能拦住它前提是校验真的跑了。四、检索与知识源的统一一份索引多入口消费检索不一致里最常见的一类是索引版本不同。同一个知识库网页端用的是上周重建的索引客服工作台用的还是上个月的问同一个问题召回片段自然不同。治理原则很直白一份索引多入口消费。不要在入口这一层各自建索引入口只决定用什么过滤条件去查不决定查哪份数据。如果入口确实需要不同的知识范围比如客服只看售后文档API 用户能看全部那也应该是同一份索引上的不同过滤视图而不是两份物理索引。两份索引意味着两套重建流程、两套版本号偏差的种子就此埋下——而且它们会在你最不希望的时候分叉一次只重建了一半。过滤条件要显式声明不能藏在代码里第二个常见来源是过滤条件隐含在代码里。某个入口的检索函数里悄悄加了一句doc_type help另一个入口没有。这种差异在代码评审里极易滑过去因为它是「一行 if」。做法是把过滤条件从代码里提出来作为入口配置的一部分显式声明⚠️ 代码待验证# 过滤条件来自入口配置不允许出现在检索函数体内ENTRY_FILTERS{web:{},# 全量api:{doc_type:[help,faq]},support:{doc_type:[after_sale],region:[cn]},# 客服只看中文售后im_bot:{doc_type:[faq]},}defsearch(entry:str,query:str,top_k:int8)-list:fltENTRY_FILTERS[entry]# 键不存在直接 KeyError逼你登记returnindex.query(query,filterflt,top_ktop_k)这里用ENTRY_FILTERS[entry]而不是.get(entry, {})是故意的新入口没登记过滤条件时应该直接报错而不是悄悄退化成全量检索。后者看起来兜住了实际是把一个「该被发现的配置缺失」变成了一个「永远查不到的不一致」。索引版本要跟着答案一起记录这节和第二章是连着的。第二章打点里的index_version和retrieval_filter不是摆设它们的作用就是在出现不一致时能直接回答「两个入口是不是查了同一份数据、用了同一条件」。索引重建完成后各入口的引用要统一切换而不是各切各的。切换前后留一份记录是后面做对照跑时的基线。五、状态与会话的取舍哪些入口共享会话、哪些不共享多入口还有一个容易被忽视的分叉源状态。网页端有会话上下文API 侧无状态IM 里一件事就是一条消息。用户在网页聊了五轮再来 App 问看到的是两份互不知情的历史——他会觉得「你怎么又不知道了」。先明确哪些入口共享、哪些不共享。这个判断的依据是入口的交互模型不是技术方便程度入口组合是否共享会话原因网页 ↔ App共享同一个人、同一账号、连续交互网页 / App ↔ API不共享API 调用方自己管上下文网页 ↔ IM 群不共享IM 是单条消息粒度语义不连续客服工作台 ↔ 网页默认不共享可显式拉取工单上下文与个人会话是两回事判断标准可以浓缩成一条用户是否认为这两处是「同一次对话」。用户觉得是你就得接续用户不觉得硬接续反而会让他困惑——把客服工单里的内容带进个人对话那是隐私事故不是贴心。跨入口接续的最小传递物共享会话不等于共享全部历史。全量历史搬来搬去有两个坏处状态对象越来越重、跨入口传递的内容越来越不可控。最小传递物应该只包含接续所必需的东西而不是整段对话⚠️ 代码待验证{session_id:s_8f21,owner:user_1024,entries_seen:[web,app],summary:用户在问报销流程已确认所在地区未确认发票类型,open_slots:[invoice_type],last_active:1760000000,turn_refs:[t_91,t_92,t_93]}这份结构里有三个字段值得单独说summary是压缩后的接续状态不是原文。跨入口传原对话既贵又难控制裁剪而且原文里可能有不该跨入口流动的内容。open_slots记录了「还没问清楚的点」。App 端接上时可以直接接着问那个缺口而不是从头把信息问一遍。turn_refs是引用而非内容。需要详情时按 id 回查避免状态对象随轮数膨胀。不共享时怎么把历史带过去对不共享的入口比如 IM 群处理方式不是把会话接过去而是把结论带过去。用户在 IM 群里问一个问题机器人可以给出答案但如果用户接着问「那我上次说的那个呢」IM 侧无从上文取到。可接受的折中是允许用户显式地引用一段对话给另一个入口作为这一次请求的输入而不是把这个入口变成隐式共享会话的一端。显式的引用在隐私上可控——用户知道自己带过去了什么隐式的共享则不可控也违背 IM 单条消息粒度的模型。这里有个顺序上的讲究先定「哪些入口共享」再定「传递物里放什么」。顺序反过来的话你会先设计出一个很全的状态对象然后发现没有任何一个入口真的需要它写出来的字段最后全是没人读的。完整版资料清单本文用到的入口对照表与归因实测记录都整理在里面了扫码即可获取六、缓存必须有统一口径键怎么设计才不串缓存是一致性里最隐蔽的一处。命中缓存的入口返回旧答案没命中的返回新答案两边看起来都「正常」只有用户能看出不对。问题的根在键。如果键只由问题文本拼成那么任何入口问同一个问题都会命中同一条缓存版本、提示、检索的变化全被盖住。正确的键要把影响答案的变量都装进去⚠️ 代码待验证importhashlibdefbuild_cache_key(question:str,ctx:dict,include_entry:bool)-str:parts[ctx[model_version],# 版本变了缓存必须失效ctx[prompt_hash],# 提示变了缓存必须失效ctx[index_version],# 索引重建了缓存必须失效question.strip().lower(),]ifinclude_entry:parts.append(ctx[entry])# 是否让入口进键见下一节returnans:hashlib.sha256(|.join(parts).encode()).hexdigest()[:24]四个变量的前三个必须进键这一点没有争议——它们任何一个变了答案就不该复用旧缓存。有争议的是第四个入口要不要进键。入口标识能不能进键把入口标识拼进键直觉上更安全但它会直接拉低命中率所以不能无条件做。判断方法很简单两个入口在给定问题上是否会因为入口差异给出不同答案。会就必须进键不会就不该进。几种典型情况分开看。各入口提示不同时提示指纹已经在键里了入口标识进不进都能正确失效进键只是让排查时更直观各入口检索过滤不同时过滤条件本身已经在键里同样不必重复各入口输出格式不同时格式不同就意味着答案文本不同这时入口标识必须进键。反过来看如果几个入口同源同配置把入口拼进键只会让本该共用的缓存被切成几份白白降低命中率。早先「入口一定进键」和「入口绝不进键」两种一刀切都不对前者白白拉低命中率后者在大改提示之后还可能继续返回旧答案。敏感入口不共享缓存的判据有一类入口无论变量是否相同都不应该和别人共享缓存涉及个人数据、涉及权限隔离、涉及工单上下文的入口。判据是答案里是否包含只对某个用户或租户可见的内容。如果客服工作台的答案里会带出当前工单的编号和客户信息那么它的缓存键只由变量和问题拼成是危险的——不同工单的同一问题会互相串。这类入口应该直接关闭共享缓存或者把租户标识也拼进键。反过来那种纯知识型、答案对所有用户相同的入口共享缓存不仅能提命中率还能顺带把答案口径统一了——它们命中同一条缓存就不可能有分歧。缓存在这里既是性能手段也是一致性手段。七、怎么知道现在一致了同问题集对照跑前面六章做了一堆收敛动作但「现在一致了吗」这个问题不能靠感觉回答。要有一个同问题集 同输入口径的对照跑法。问题集不是随便凑的应该来自两处一是第二章归因记录里真正出现过分歧的问题二是各入口的高频问题。前者覆盖已知的老毛病后者覆盖最可能被用户撞见的场景。两者加起来一份几十条的问题集就够起步。⚠️ 代码待验证# 对同一批问题逐入口跑一遍收集四变量与答案forentryinweb app api support im_bot;dopython run_probe.py\--entry$entry\--questionsquestions.jsonl\--record-fields model_version,prompt_hash,index_version,cache_hit\--outprobe_${entry}.jsonldone# 比对同一问题在不同入口的答案是否一致python compare_probe.py--inputsprobe_*.jsonl --group-by question--outdiff_report.csv--record-fields是关键参数。只比对答案文本是不够的——不一致出现了还要能立刻知道是哪个变量分的叉所以四个变量必须跟答案一起落盘。跑之前要清一次缓存否则上一次的缓存命中会污染这一轮的判定。按入口维度看差异率对照跑的产出是一张按入口对看的差异率表。看的时候要带上变量维度否则只看到一个百分比等着你去猜。入口对差异率主要归因变量动作网页 ↔ App高版本App 发布滞后收敛发布节奏网页 ↔ API中提示调用方自带底座兜底网页 ↔ 客服中检索过滤差异显式声明过滤条件App ↔ API低缓存键含入口保留现状差异率不是要压到 0。有些分歧是设计允许的——API 入口按调用方传入的格式约束输出本来就和网页不同。要做的是把允许的分歧和意外的分歧分开只追后者。把允许的分歧也塞进分母会让这个指标永远不达标然后被所有人无视。把差异率接进发布前检查对照跑如果只是偶尔手动跑一次它迟早会变成一个没人看的报告。要做的是把它接进发布流程任何改动底座提示、模型版本、索引重建的发布都在发布前跑一次对照跑差异率超过阈值就拦住。阈值怎么定也有讲究。一开始的阈值应该宽等差异率稳定在某个水平之后再把它降到「比当前水平略高一点」让它去抓的是回归而不是幻想中的零差异。一个从一开始就设成 0% 的阈值唯一结果是所有人都学会绕过它。完整版资料清单本文用到的入口对照表与归因实测记录都整理在里面了扫码即可获取附表 A关键取舍一览本文涉及的所有工程判断集中在这里方便按需回看。取舍本文结论判断依据位置不一致的定性默认状态不是偶发故障四处变量叠加一致概率低于半数第一章归因前提四个变量各自打点不拆开就只能是猜第二章归因动作一次只动一个变量同时改两个无法定位第二章提示结构共享底座 入口差异化片段底座必须全入口唯一第三章差异化范围走白名单越界报错安全边界不允许入口差异第三章配置存放可 diff 的文件纳入发布评审散落各处无法比对第三章索引形态一份索引多入口消费多份索引等于多套版本第四章过滤条件从代码提到入口配置显式声明藏在代码里极易漏审第四章跨入口会话按「是否同一次对话」判断硬接续会造成隐私事故第五章接续传递物摘要 缺口 引用不传原文全量历史太重且不可控第五章缓存键版本/提示/索引必须进键三者任一变化都该失效第六章入口是否进键按是否会造成答案差异决定一刀切都不对第六章敏感入口缓存关闭共享或拼入租户标识答案含个人可见内容会串第六章一致性判定同问题集 同输入口径对照跑靠感觉判断不可复查第七章差异率阈值先宽后收抓回归不追零定成零会被绕过第七章附表 B术语速查表术语含义入口消费同一个 AI 能力的产品形态如网页、App、API、IM 群四处来源多入口不一致的固定成因版本、提示、检索、缓存变量打点每次回答都记录四个变量取值供归因使用提示底座所有入口必须共享的提示部分承载身份与安全边界差异化片段入口特有的提示片段范围受白名单约束差异化白名单允许入口自行调整的维度清单一份索引知识库物理上只有一份索引入口只改变过滤视图显式过滤条件检索过滤条件登记在入口配置中不写在检索函数里会话接续把共享会话的状态从一个入口传递给另一个入口最小传递物接续所需的最小状态集合摘要、缺口、轮次引用开放槽位会话中尚未问清、需要继续追问的点缓存键决定缓存命中与否的字符串由影响答案的变量拼接而成对照跑用同一问题集、同一输入口径逐入口跑一遍的一致性检查差异率同一问题在不同入口答案不一致的比例发布前检查把对照跑接进发布流程差异率超阈值则拦截写在最后这篇用到的资料写这篇文章时我把多个入口的一致性来源与排障路径重新梳理了一遍顺手整理成几份配套的东西大模型学习路线图从 LLM 基础到 Agent 开发各阶段该学什么、用什么资料大模型全套教程按主题分好的视频与文档清单大模型实战好书24 本附每本适合的阶段资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「大模型」优先通过。拿到之后建议先看学习路线图那一份先定位自己在哪个阶段再决定学什么比一上来就啃框架效率高得多。