一个聊天产品如果只调用一次模型,提示词可以写得很长、很全。但真正上线后,同一条用户消息往往要同时完成好几件事:像朋友一样回复、判断是否要引导测评、检测情感操控风险、匹配用户自己记录过的雷点、抽取长期记忆、更新关系分数、生成标题和后续问题。
这些事情看起来都发生在“聊天”里,实际上目标、输出格式和失败代价完全不同。把它们塞进一个提示词,短期能跑,长期一定会出问题。
核心结论多提示词不是为了制造“多 Agent”的复杂度,而是因为不同任务的目标、输出契约和失败代价不同。编排权应该留在业务代码里,模型只负责语义判断。
1. 先纠正一个说法:这是“多节点编排”,不是“自治 Agent”
很多面试里一提到多 Agent,默认指的是自主规划、工具调用、Agent 之间互相传递消息的框架。这个项目不是那种形态。
更准确的定义是:一个主生成模型,加上若干单职责的 LLM 节点,由业务代码按固定流程编排。
| 维度 | 自治 Agent | 这里的多节点编排 |
|---|---|---|
| 下一步做什么 | 模型自己决定 | 业务代码决定 |
| 节点之间 | 可以互相调用、协商 | 不通信,只交换结构化结果 |
| 工具调用 | 有 | 基本没有,主要是分类、抽取、打分 |
| 可预测性 | 较低 | 高,链路固定 |
| 可观测与计费 | 较难归因 | 每个节点单独计时、单独计费 |
| 扩展新能力 | 加工具或加 Agent | 改编排器,加一个节点 |
这个区分很重要。它不是“能力更弱的 Agent”,而是为生产环境做的取舍:链路可预测,失败可隔离,成本可归因。代价是灵活性,后面会专门讲。
2. 单一提示词会坏在哪
2.1 目标冲突,人格会污染判断
主对话的人格要求是:像好朋友、先共情、站用户这边、别像咨询师、别输出报告。
而风险检测的要求正好相反:冷静、中性、严格 JSON、不要安慰、不要道德判断。
如果共用一套提示词,模型会带着“站用户这边”的倾向去判风险。结果要么把正常冲突判成操控,要么因为怕伤害用户而漏报。安全判定被陪伴人格带偏,这是产品上不能接受的。
2.2 输出契约冲突
主回复是流式自然语言,还要按分隔符切成多个聊天气泡。辅助任务要的是能被代码直接解析的 JSON,例如三态风险结论、命中项 ID、数值 delta。
一个提示词同时要求“自然聊天”和“只输出 JSON”,模型一定会在某一边妥协。实际表现就是:要么回复里冒出 JSON,要么检测结果带一堆解释文字导致解析失败。
2.3 延迟和成本被绑死
主对话必须尽快出第一个字。意图分类、标题、后续问题这些任务根本不值得用最贵的模型。
如果所有事都塞进主调用,等于每条用户消息都按主模型的成本和时间跑一遍,而且主模型的上下文会被大量与回复无关的指令占满。
2.4 提示词无法独立迭代
在这个产品里,提示词是运营内容,不是代码常量。今天调风险判定的尺度,明天调人格语气。
如果只有一个大提示词,任何一次微调都可能把主对话改坏,而且没法单独回滚,也无法判断改动到底影响了哪一部分。
2.5 失败代价不同
主回复失败是用户可见的故障;风险检测失败应该静默降级;记忆抽取失败可以下次再补。
把它们绑在同一次调用里,等于让“低价值、可失败”的任务拥有了拖垮“高价值、不可失败”任务的能力。这是架构上最不能接受的耦合。
3. 拆分依据不是“功能多”,而是三条差异
拆分时真正看的不是业务上有多少功能,而是:
- 目标是否冲突:共情生成和风险判定不能互相污染。
- 输出契约是否不同:流式自然语言和严格 JSON 是两种协议。
- 失败代价是否一样:主回复失败是用户可见故障,辅助检测失败应该静默降级。
按这三条,最终拆成五类节点。
| 节点类型 | 职责 | 输出 | 失败代价 | 典型超时 |
|---|---|---|---|---|
| 主生成 | 人格对话 | 流式自然语言 | 高,主链路 | 不设短超时 |
| 路由分类 | 是否要做关系测评 | 严格 JSON | 低,可规则兜底 | 约 2.5 秒 |
| 安全判定 | 情感操控风险、雷点命中 | 严格 JSON | 低,降级为“证据不足” | 约 8 秒 |
| 抽取与打分 | 长期记忆、关系分数 | 严格 JSON | 低,可跳过或下次重试 | 后台任务,按批次 |
| 结构化产出 | 标题、后续问题、测评报告 | 文本或 JSON | 中低,可留占位或按需生成 | 各不相同 |
一个反过来的判断标准:如果两个任务目标一致、输出格式一致、失败代价也一致,就不要拆。 多节点的复杂度是真实的,拆分应该由契约差异驱动,而不是由“看起来更像 Agent”驱动。
4. 一条用户消息的完整链路
以用户发来一句“他三天没理我了,我是不是太敏感”为例。
用户消息落库 │ ▼编排器 ├─ ① 意图识别(同步,可短路) │ 规则快路径 → 命中则直接推测评引导,主模型不启动 │ 否则小模型分类 → 命中同样短路 │ ├─ ② 并行扇出两个异步检测(不阻塞主回复) │ ├─ 情感操控风险:看整段会话 │ └─ 雷点匹配:看最新消息 + 用户画像里的活跃雷点 │ ├─ ③ 额度校验 ├─ ④ 上下文装配(人格、记忆、关系分数、RAG、历史消息) ├─ ⑤ 主模型流式生成 → 增量下发 → 切气泡 → 落库 → 扣费 ├─ ⑥ 首轮标题生成 ├─ ⑦ 回收 ② 的结果(带超时)→ 命中则追加告警卡片 └─ ⑧ 后置测评引导判定 → done4.1 消息先落库,再开流
用户消息先写入数据库,拿到稳定的消息 ID,前端再用这个 ID 建立 SSE。
这样即使模型调用失败,用户消息不会丢,重试也有稳定锚点。流式回复和消息持久化解耦,是后面所有失败处理的前提:告警卡片、扣费、记忆抽取都要挂在这条消息上。
4.2 意图识别:规则优先,模型兜底
第一步不是直接调模型,而是先跑正则快路径。像“做测试”“我想测评”“take the test”这种明确表达,直接命中,不产生模型调用。
只有模糊表达才走小模型分类,要求只返回类似 {"intent":"TAKE_RELATIONSHIP_ASSESSMENT"} 或 {"intent":"NONE"} 的结果。模型超时或报错,再退回关键词兜底;还不行就当作没有意图,继续正常聊天。
如果命中意图,主链路直接短路:不生成聊天回复,改为保存一条测评引导消息推给前端。
这一步的价值是:能用规则判的绝不花钱调模型,模型只处理语义模糊的地带。它也是整条链路上唯一一个同步等待的辅助节点,所以超时预算必须卡得很紧,通常配置在两秒多。
4.3 风险检测:并行、异步、与主回复隔离
在组装主上下文之前,两个风险检测任务已经并行发出去了。
情感操控风险检测的输入是当前会话里所有用户消息,因为操控往往是模式,而不是单句。输出是三态:命中、未命中、证据不足,并附带一句原因和少量关键词。同一个关系画像同一个本地日期只告警一次。
雷点检测的输入是最新一条用户消息,加上用户画像里最多几十个活跃雷点。模型只能从给定的雷点里选一个 ID,不能自己编。
两个任务都在主模型调用期间并行执行,等主回复落库之后才去收集结果。任何一个超时或异常,统一降级成“证据不足”,不打断聊天。
这里有两个容易被忽略的工程细节:
第一,去重不能靠“先查再插”。 每日去重靠的是“关系画像 + 本地日期”的唯一约束。两个标签页同时发消息时,后到的请求会撞唯一约束。系统把这种数据库冲突当成“今天已经告过警”正常返回,而不是当系统错误。如果不处理,日志里会全是假告警,运营也没法判断告警是不是真的坏了。
第二,异步结果不能反过来影响本轮回复。 这是有意的取舍。检测是异步的,主回复已经开始流式输出,结果只能在本轮之后追加告警卡片。如果要求检测影响回复内容,就必须同步阻塞,首字延迟会明显变差。产品上选择了“先回复,再补风险提示”。
4.4 主上下文装配:分层、过滤、预算
主对话看到的不是“历史消息 + 用户问题”,而是多层拼装:
| 层 | 内容 | 关键约束 |
|---|---|---|
| 人格 | 朋友语气、气泡格式、长度自适应、安全边界 | 可由运营在后台发布新版本 |
| 记忆 | 用户画像里的雷点与甜蜜点 | 有条数和字符预算,默认约 30 条 / 2000 字符 |
| 语言 | 必须跟用户最新消息同语言 | 单独一条强指令,防止中英混杂 |
| 关系状态 | 最新多维度分数 | 明确标注“软上下文,不是诊断” |
| 附件 | 用户上传的聊天记录归档 | 作为独立上下文块 |
| 检索 | 与最后一条用户问题相关的知识 | 只用最新问题检索,命中才注入 |
| 历史 | 最近若干条消息 | 作为对话基底 |
几个值得讲的细节:
记忆放在人格里,还是单独一条系统消息? 人格模板里可以保留一个记忆占位符。如果模板写了占位符,记忆会被渲染进人格提示词内部;如果没写,才作为独立系统消息追加。这是提示词层面的选择:记忆放在人格里,模型更愿意把它当“我对这个人的了解”,而不是一条外挂指令。
卡片消息必须过滤掉。 数据库里除了正常对话,还有测评引导、风险卡片、雷点卡片、雷点图片。这些在拼上下文时会被排除。否则模型会把自己系统生成的 UI 卡片当成对话内容,回复就会变得很奇怪。
检索是混合的,不是纯向量。 查询同时走向量表示和词法表示,做混合召回,再按最低分阈值过滤。这样既能处理语义相近,也能兜住关键词精确命中;召不回就当作没有检索结果,不阻塞主流程。
4.5 流式生成与气泡切分
主模型流式返回。这里有一个很细的坑:系统要求模型用特定分隔符分隔聊天气泡,但流式 chunk 是按字节边界来的,分隔符很可能被切成两半。
所以流式转发时会在尾部保留一小段字符不立即发,等确认不是半个分隔符再放出去。生成结束后再按分隔符切分,最多保留四段;如果模型没有输出分隔符,就整段当一条消息。
这样前端拿到的是一组气泡,而不是一大段文字。这个细节看起来小,但如果不做,用户会偶尔看到被截断的怪字符,或者气泡分错。
4.6 落库、扣费与失败回滚
回复完整后先落库,再按模型返回的 token 用量扣费。
如果扣费时发现余额不足,会把刚保存的助手消息删掉再抛错。不能让用户看到一条“已经生成但没付费”的消息,也不能让后续上下文里留着一条失败的回复。
4.7 回复后处理
主回复已经发给用户之后,才去等风险检测的结果。命中就保存告警卡片消息并推送事件;未命中、超时、解析失败都静默结束。
标题生成只在会话标题仍是占位符、且这是第一轮对话时触发。输入是用户第一句和助手第一段回复,要求输出短标题,限制字符数,并去掉引号和结尾标点。失败就保留占位,用户无感。
后置测评引导是另一条业务规则:不是每轮都推,只在满足条件时追加,避免把聊天变成广告位。
4.8 不在关键路径上的节点
以下能力不在聊天关键路径上:
- 记忆抽取:定时扫描,用户静默几秒后才处理,避免每句话都调模型。用游标记录“上次处理到哪条消息”,只处理增量;写入前还会按消息 ID 去重,防止即时雷点检测和后台抽取重复计数。
- 关系分数更新:对增量用户消息按本地日期分组,逐日做多维度 delta。每个 delta 有上下限,并且不能把总分推出合法区间;格式解析失败会带提示重试一次,仍失败就把这一天标记为已处理并继续,不卡住后面的批次。
- 后续问题:不是主 SSE 的一部分,由前端单独请求;只有助手回复超过一定长度才生成,短回复不浪费调用。
- 关系测评:规则引擎先算分,分数立刻以预览事件推给前端;模型只负责把分数解释成叙述报告,提示词里明确写“规则分数是权威值,不许改分”。报告落库时再让模型润色正文,失败则回退到规则引擎给出的固定文本。
5. 两条工程底线
5.1 模型做判断,代码做裁决
最终结论不交给模型:
- 雷点检测让模型从给定列表里选 ID,代码再校验这个 ID 确实存在、是活跃的、类型匹配。模型编一个 ID 出来会被拒绝。
- 关系分数不是让模型自由给分,而是给当前分、给 delta 上下限,模型只能在这个范围里调。
- 测评分数由规则引擎算,模型只负责解释。提示词里写死“分数是权威值”。
- 意图识别命中后,代码再决定是否真的短路,而不是模型直接控制流程。
一句话:模型负责语义理解,业务口径和边界由代码守住。
5.2 假设模型会不听话
提示词里写“只输出严格 JSON”,但代码从不假设它真的只输出 JSON:
- 先去掉 Markdown 代码围栏;
- 从文本里截取第一个左花括号到最后一个右花括号;
- 字段缺失、类型不对、枚举值不认识,统一落到“证据不足”或默认值;
- 解析失败不抛到主链路。
这是多调用系统能稳定的前提:每个节点都自带降级路径,而不是指望上游永远正确。模型是一个会犯错的协作者,不是一台确定性函数。
6. 超时、并发与降级
不同节点的超时预算必须分开:
| 节点 | 预算 | 超时后的行为 |
|---|---|---|
| 意图识别 | 两秒多 | 当作没有意图,继续正常聊天 |
| 情感操控检测 | 约八秒 | 返回“证据不足”,不告警 |
| 雷点检测 | 约八秒 | 返回“证据不足”,不告警 |
| 主回复 | 不设短超时 | 主链路错误,按 SSE 错误事件返回 |
| 记忆抽取 | 后台批次 | 记审计,游标照常推进 |
| 关系分数 | 后台批次 | 该日标记已处理,继续下一天 |
| 后续问题 | 按需请求 | 前端拿到空列表 |
| 标题生成 | 短任务 | 保留占位标题 |
编排层等待异步结果时,会比配置的超时再多留一点余量,避免刚好在边界上误判。主模型不设这种短超时,因为它是主链路,短超时只会制造假失败。
关键不在于“调用多所以更不稳定”,而在于这些调用不在同一条失败链上。主回复只依赖主模型,其余节点失败即降级。
还有一个跨渠道的并发细节:网页、WhatsApp、Telegram 共用同一套编排。同一个外部会话里如果连续来两条消息,系统会按会话串行化处理;新消息会标记旧回复为过期,旧回复即使生成到一半也会被丢弃,避免两条回复交叉到达。这是“同一会话同一时刻只有一个有效回复”的约束,不是靠模型,而是靠编排层的租约和过期标记。
7. 提示词作为可发布资产
提示词存在数据库里,而不是散落在代码常量中。
7.1 版本模型
每个提示词有模板和多个版本,版本状态分为草稿、已发布、已归档。
- 发布新版本时,旧版本自动归档。
- 回滚不是把旧版本直接改回“已发布”,而是复制成一份新草稿再发布。这样历史版本永远不可变,审计链是干净的。
- 保存草稿时会校验:系统提示词和用户提示词不能同时为空、单段长度不能超过上限、扩展字段必须是合法 JSON。
- 发布动作会写审计日志,记录是谁、在什么时候、发布了哪个版本。
这套机制解决的是“提示词谁来改、改坏了怎么退”。如果提示词只写在代码里,运营每次调语气都要发版,这是不现实的。
7.2 运行时读取与缓存
运行时优先读数据库中的已发布版本,读不到再回退到代码默认值。结果按较短 TTL 缓存在内存里,发布时主动失效。
这样既保证了运营改完能较快生效,又避免每次对话都查库。回退到代码默认值这一点也很重要:数据库里没有模板、或者读取异常时,聊天不能因此不可用。
7.3 环境间同步
生产环境的提示词可以导出成快照文件,本地开发启动时自动应用到开发库。这个过程不通过管理接口,也不重启服务,而是直接对比快照和本地库,缺的创建、不一致的归档旧版并发布新版、一致的跳过。
应用前支持 dry-run,先看会创建几条、更新几条、跳过几条。另外保留一份候选快照用于实验。评估脚本只读快照,不写数据库、不调管理接口,所以可以放心在本地反复跑。
8. 用回归集约束提示词改动
提示词改动不能靠“感觉更好”。主对话人格有一套离线评估:
- 固定用例集:每条用例带分类、标签、期望行为、必须出现什么、绝不能出现什么、触发否决的条件。
- 加权评分标准:例如自然度 25、保护性立场 20、情绪贴合 20、口语亲密感 15、安全边界 10、可回复性 10。
- 全局否决规则:鼓励报复或骚扰、在聊天里自造测试题、语言严重错配、无证据断言对方出轨或恶意、输出报告体而不是聊天。任何一条命中,直接判负,不参与加权。
- 生成模型和裁判模型分开配置:可以换更便宜或更严格的裁判。
- 评估输出:每个用例的基线回复、候选回复、裁判打分、胜负与否决命中,最后汇总成 JSON 和 Markdown。
- 支持复用已有回复只重跑裁判:调评分标准时不用重新生成一遍,省时也省成本。
还有一个写进文档的约束:只有评分标准版本、用例集、裁判模型、裁判提示词都一致时,结果才可以横向比较。否则分数没有意义。这一点比工具本身更重要,很多团队的评估最后变成“每次跑出来的数都不一样,但没人知道为什么”。
这套东西的意义是:提示词改动有回归、有否决、有可比较的结果,而不是拍脑袋。
9. 观测与成本归属
每次调用都会记录:走了规则还是模型、是否回退、耗时、输入输出字符数、使用哪个模型。
计费通道也按能力拆开:主对话、记忆、后续问题、意图、标题、关系分数、风险检测各算各的。
这带来两个好处:一是能看出钱花在哪;二是出问题时能快速定位是哪个节点慢、哪个节点在疯狂回退。
如果没有按能力拆分的计费,只看到一个总 token 数,你会很难回答“到底哪个功能最贵”这个问题。拆开之后,优化才有靶子。
10. 这套设计的代价
多提示词不是免费的。
- 调用链变长,维护成本变高。 多个提示词、多套配置、多个降级分支,比一个大提示词难维护。
- 一致性风险。 多个节点各自输出,可能出现语言或口径不一致,需要靠统一语言指令和版本化模板缓解。
- 没有全局规划能力。 节点之间不互相推理,跨任务的复杂决策仍然靠业务代码。这对当前产品够用,但如果要做开放式多步任务,就需要引入真正的规划器。
- 意图识别仍在关键路径。 虽然有规则快路径和超时,极端情况下还是会影响首字延迟。
- 扩展新能力要改编排器。 没有统一的节点注册和路由机制,新增一个节点意味着改主流程,而不是写个插件挂上去。
- 观测仍然是分散日志。 目前靠会话 ID 和消息 ID 关联,没有统一的端到端 trace,跨节点排查仍然要靠人拼时间线。
11. 什么时候值得这样拆
如果满足下面任意两条,就值得考虑拆分:
- 同一份输入需要多种互相冲突的语气或立场;
- 一部分任务要求严格结构化输出,另一部分要求自然语言;
- 不同任务的延迟、成本、模型能力要求差异明显;
- 不同任务的失败代价不同,不能一起失败;
- 提示词需要由非开发人员频繁调整,并且要能独立回滚。
反过来说,如果所有任务目标一致、输出格式一致、失败代价也一致,那就没必要拆。多节点带来的复杂度是真实的,拆分应该由契约差异驱动,而不是由“看起来更像 Agent”驱动。
12. 怎么把它讲给别人听
如果只有一分钟,可以按三层讲。
第一层,一句话: 这不是一个会自己规划的多 Agent 系统,而是一个主生成模型加多个单职责节点的固定编排。拆分的依据是目标冲突、输出契约不同、失败代价不同。
第二层,一张图: 消息先落库,意图识别可以短路;两个风险检测并行异步跑;主模型流式回复;回复后再回收检测结果;记忆、评分、后续问题都在关键路径之外。
第三层,三个取舍:
- 同步还是异步:只有意图识别在关键路径上,因为它决定要不要回复;其余检测全部异步,失败不影响主回复。
- 模型还是代码:模型做语义判断,代码做边界裁决。ID 校验、分数上下限、规则算分都留在代码里。
- 自治还是编排:选择固定编排,换取可预测、可观测、可计费;代价是灵活性,新增节点要改主流程。
如果再被追问“为什么不用一个大模型全做完”,就回到那三条差异:人格会污染安全判断,自然语言和严格 JSON 是两种协议,主回复失败和检测失败不该共担风险。
一句话总结这套设计的本质不是“多 Agent”,而是把不同输出契约、不同失败代价、不同迭代节奏的 LLM 能力拆成可独立运营的节点;模型负责语义判断,代码负责流程、边界和裁决。