检索结果、工具数据、对话历史——最终的Prompt怎么拼
开篇引言
上一篇把 MCP 参数提取器的内部实现拆干净了,MCP 工具调用子系列到此收尾。回头看一下,到第 13 篇为止,前面所有阶段的数据都已经备齐:
- 第 2、3 篇产出了会话记忆——对话摘要加上最近几轮的 history
- 第 4 篇产出了改写后的子问题——指代消解、上下文补全后的检索友好查询
- 第 5~9 篇产出了意图识别结果——命中了哪些 KB 节点、哪些 MCP 节点,每个节点上挂着什么
promptTemplate和promptSnippet - 第 10、11 篇产出了5 条精排后的 KB 检索结果——三个通道 30 条粗排,后处理流水线压到 5 条
- 第 12、13 篇产出了MCP 工具执行结果——一份结构化的销售报告、一条用户年假记录,或者别的什么业务数据
这些数据现在分别在 RetrievalContext 的 kbContext、mcpContext、intentChunks 几个字段里,加上 StreamChatContext 里的 history 和 rewriteResult。但大模型只认一样东西——一个 messages 数组。怎么把这堆零件拼装成大模型能消化的形态,就是本篇要讲的事。
为什么要分场景拼 Prompt
1. 三种原料,三种吃法
直觉上,写一套万能模板似乎就够了——不管什么情况,把检索结果和工具数据一股脑塞进去。但实际跑起来会出问题。
KB 检索结果是文档片段,模型需要被告知严格基于文档回答,不能编造,链接和图片要保持原格式。MCP 工具数据是结构化 JSON,模型需要被告知把字段名转成业务术语,隐私字段要脱敏,空数据要有合理的兜底回复。如果只有 KB 结果,塞一堆 MCP 相关的规则(JSON 转述、隐私脱敏)就是噪音;反过来,只有 MCP 数据时,塞 KB 的块级引用约束也毫无意义。
所以 Ragent 把 Prompt 组装分成了三个场景,每个场景用不同的 System Prompt 模板:
| 场景 | 判定条件 | System Prompt 模板 | 侧重点 |
|---|---|---|---|
KB_ONLY | 只有 KB 检索结果 | answer-chat-kb.st | 文档问答:块级引 用、禁止编造、链接图片处理 |
MCP_ONLY | 只有 MCP 工具数据 | answer-chat-mcp.st | 数据转述:JSON → 自然语言、隐私脱敏、异常处理 |
MIXED | KB 和 MCP 都有 | answer-chat-mcp-kb-mixed.st | 综合回答:实时数据准确性优先于文档、资讯等 |
还有一个特殊情况——所有意图都命中了 SYSTEM 类型节点(打招呼、自我介绍等),这在第 1 篇讲流水线时提过,handleSystemOnly() 会提前短路,压根不走 Prompt 组装这条路。
2. 场景判定的代码
场景判定的逻辑在 RAGPromptService.plan() 方法里,简洁到只有四个 if:
private PromptBuildPlan plan(PromptContext context) {
if (context.hasMcp() && !context.hasKb()) {
return planMcpOnly(context); // 只有 MCP
}
if (!context.hasMcp() && context.hasKb()) {
return planKbOnly(context); // 只有 KB
}
if (context.hasMcp() && context.hasKb()) {
return planMixed(context); // 两者都有
}
throw new IllegalStateException(...); // 不可能走到这里
}
判定依据就是 PromptContext 上的两个方法——hasMcp() 和 hasKb(),各自检查对应的上下文字符串是否非空。到这一步时,检索引擎已经执行完了,结果有没有就是一个字符串是否为空的事。
为什么不会出现两者都为空的情况?因为流水线在
retrieve()之后有一个handleEmptyRetrieval()短路点——如果检索结果全空,直接回复未检索到与问题相关的文档内容就结束了,不会走到 Prompt 组装。