prompt工程
假设你在维护一个电商客服知识库,用户问:iPhone 16 Pro Max 拆封后还能退吗?
经过混合检索 + 重排序,Top-1 返回的 chunk 是:
iPhone 16 Pro Max 因屏幕定制工艺,拆封后不支持七天无理由退货。如需退货,需经售后检测确认存在质量问题。
chunk 找得很准,完全命中了用户的问题。但你把这个 chunk 和用户问题一起丢给大模型,模型的回答有可能是:
场景一:凭空捏造细节
用户问:退货运费谁承担?
chunk 内容:质量问题退货,运费由商家承担;非质量问题退货,运费由买家承担。
模型回答:退货运费由商家承担,一般会在 3-5 个工作日内退还到您的账户,退款金额包含商品价格和运费。
chunk 里只说了运费由商家承担,没提退款时效和退款范围,但模型自己补了 3-5 个工作日和退款金额包含商品价格和运费这些细节。如果实际退款时效是 7-15 个工作日,这个回答就会误导用户。
场景二:张冠李戴
用户问:AirPods Pro 的保修期是多久?
检索返回了两个 chunk:
- chunk 1:AirPods Pro 保修期为 1 年,自购买之日起计算。
- chunk 2:AppleCare+ 服务计划可将保修期延长至 2 年,并提供意外损坏保修。
模型回答:AirPods Pro 的保修期为 2 年,自购买之日起计算。
模型把 chunk 2 的 2 年和 chunk 1 的自购买之日起计算混在一起了。实际上 2 年是 AppleCare+ 的延长保修,不是默认保修期。
场景三:答非所问
用户问:我的订单什么时候发货?
chunk 内容:标准订单在付款成功后 48 小时内发货,预售商品以详情页标注的发货时间为准。
模型回答:我们的物流合作伙伴包括顺丰、圆通、中通等,全国大部分地区可实现次日达。如需查询物流信息,请登录 APP 查看订单详情。
用户问的是:什么时候发货,chunk 里有明确答案(48 小时内),但模型跑去介绍物流合作伙伴了。
看完这几个例子,你会发现 RAG 生成阶段面临三个核心挑战:
| 挑战 | 表现 | 后果 |
|---|---|---|
| 幻觉(Hallucination) | 模型编造 chunk 里没有的信息,或者篡改 chunk 的内容 | 用户拿到错误答案,严重时引发客诉甚至法律风险 |
| 答非所问 | 模型没有聚焦用户的具体问题,回答了相关但不对口的内容 | 用户体验差,需要反复追问 |
| 缺乏可追溯性 | 用户不知道答案的依据是什么,无法验证对错 | 信任度低,企业级场景无法通过合规审计 |
这三个问题的根源都指向同一个地方——Prompt 设计。接下来咱们就从 Prompt 的结构开始,一步步解决这些问题。
RAG 场景下的 Prompt 不是随便写一句请回答用户的问题就行的。一个设计良好的 RAG Prompt 通常由三段组成:
- 系统指令(SystemPrompt) :告诉模型你是谁、你该怎么做、你不能做什么
- 检索上下文(RetrievedContext) :把检索到的 Top-K chunk 喂给模型
- 用户问题(UserQuery) :用户的原始提问
你是一名专业的电商客服助手。你的任务是根据【参考资料】中的信息,准确回答用户的问题。
请严格遵守以下规则:
1. 只基于【参考资料】中的内容回答问题,不要使用你自己的知识。
2. 如果【参考资料】中没有足够的信息来回答用户的问题,请明确回答:"根据现有资料,暂时无法回答该问题。建议您联系人工客服获取更多帮助。"
3. 不要编造任何【参考资料】中没有提到的信息,包括数字、日期、金额等具体细节。
4. 回答时请引用参考资料的编号,格式为 [1]、[2] 等,标注在相关句子的末尾。
5. 如果多条参考资料的信息存在冲突,请指出冲突并告知用户以最新的资料为准。
6. 用简洁、友好的语气回答,避免过于官方或生硬的表述。
逐条拆解一下每条指令的作用:
| 规则 | 作用 | 解决的问题 |
|---|---|---|
| 规则 1:只基于参考资料回答 | 限制模型的知识来源,防止它用训练数据里的通用知识覆盖检索结果 | 幻觉——篡改事实 |
| 规则 2:信息不足时明确告知 | 给模型一个"兜底出口",不知道就说不知道(抑制幻觉最关键的一条) | 幻觉——凭空捏造 |
| 规则 3:不要编造具体细节 | 强调数字、日期、金额这些容易被编造的信息 | 幻觉——捏造细节 |
| 规则 4:引用参考资料编号 | 让答案可追溯,用户能验证 | 缺乏可追溯性 |
| 规则 5:处理信息冲突 | 多个 chunk 说法不一致时,模型不要自己选一个,而是告知用户 | 张冠李戴 |
| 规则 6:语气要求 | 控制回答风格,符合客服场景 | 答非所问(间接) |
常见的 System Prompt不能太短,没有约束,也不能太长,指令冲突
2. 检索上下文(Retrieved Context):把 chunk 喂给模型
拿到 Top-K chunk 之后,怎么组装成上下文喂给模型?这里有几个关键决策。
2.1 上下文的组装格式
推荐的格式是:每个 chunk 带编号、带来源信息,用明确的分隔符隔开。
【参考资料】
[1] 来源:退货政策文档 | 更新时间:2026-01-15
iPhone 16 Pro Max 因屏幕定制工艺,拆封后不支持七天无理由退货。如需退货,需经售后检测确认存在质量问题。
[2] 来源:通用退货规则 | 更新时间:2026-01-10
标准商品在签收后 7 天内可申请无理由退货,商品需保持完好,不影响二次销售。
[3] 来源:退货运费规则 | 更新时间:2026-02-01
质量问题退货,运费由商家承担;非质量问题退货,运费由买家承担。
- 编号[1][2][3] :方便模型在回答时引用,也方便后端解析引用关系
- 来源信息 :告诉模型(和用户)这条信息从哪来的,提升可追溯性
- 更新时间 :当多条 chunk 信息冲突时,模型可以根据时间判断哪条更新
- 明确的分隔符 :用空行隔开每个 chunk,避免模型把相邻 chunk 的内容混在一起理解
来源信息和更新时间来自元数据(Metadata),这就是咱们在元数据管理那一篇讲的回答可引用和回溯纠错的实际应用。
2.2 上下文窗口的限制与应对
大模型的上下文窗口是有限的。虽然现在很多模型支持 128K 甚至更长的上下文,但塞太多 chunk 进去并不是好事:
- 关键信息被稀释 :模型需要在大量文本中找到和问题最相关的部分,chunk 越多,噪音越大,模型越容易被不相关的内容干扰
- 迷失在中间(LostintheMiddle) :研究表明,大模型对上下文中间位置的信息关注度较低,排在中间的 chunk 容易被忽略
- 成本增加 :输入 token 越多,API 调用费用越高,响应延迟也越大
如果你用了重排序(Reranker),Top-K 的质量已经很高了,通常 3~5 个 chunk 就够用。宁可少给几个高质量的,也不要多给一堆低质量的。
3. 用户问题(User Query):原始问题还是改写后的问题
最简单的做法是直接用用户的原始问题。对于大多数场景,这就够了。
用户问题:iPhone 16 Pro Max 拆封后还能退吗?
但有些场景下,用户的原始问题可能不够清晰或者有歧义。比如用户问“退货怎么弄”,这个问题太模糊——是问退货流程?退货条件?还是退货运费?
这种情况可以通过 Query 改写(Query Rewriting)来优化,比如把“退货怎么弄”改写成“请问退货的具体流程和条件是什么”。但 Query 改写本身是一个独立的话题,涉及到多轮对话上下文理解、意图识别等,这一篇先不展开,后续篇章再详细讲。
4. 完整 Prompt 模板示例
把三段拼在一起,一个完整的 RAG Prompt 长这样:
【System】
你是一名专业的电商客服助手。你的任务是根据【参考资料】中的信息,准确回答用户的问题。
请严格遵守以下规则:
1. 只基于【参考资料】中的内容回答问题,不要使用你自己的知识。
2. 如果【参考资料】中没有足够的信息来回答用户的问题,请明确回答:"根据现有资料,暂时无法回答该问题。建议您联系人工客服获取更多帮助。"
3. 不要编造任何【参考资料】中没有提到的信息,包括数字、日期、金额等具体细节。
4. 回答时请引用参考资料的编号,格式为 [1]、[2] 等,标注在相关句子的末尾。
5. 如果多条参考资料的信息存在冲突,请指出冲突并告知用户以最新的资料为准。
6. 用简洁、友好的语气回答,避免过于官方或生硬的表述。
【参考资料】
[1] 来源:退货政策文档 | 更新时间:2026-01-15
iPhone 16 Pro Max 因屏幕定制工艺,拆封后不支持七天无理由退货。如需退货,需经售后检测确认存在质量问题。
[2] 来源:通用退货规则 | 更新时间:2026-01-10
标准商品在签收后 7 天内可申请无理由退货,商品需保持完好,不影响二次销售。
[3] 来源:退货运费规则 | 更新时间:2026-02-01
质量问题退货,运费由商家承担;非质量问题退货,运费由买家承担。
【用户问题】
iPhone 16 Pro Max 拆封后还能退吗?
幻觉抑制:让模型不知道就说不知道
1. 什么是幻觉(Hallucination)
幻觉这个词听起来很学术,但意思很直白:模型生成了看起来很合理、读起来很流畅,但实际上是编造的内容。
大模型的幻觉也是类似的机制。模型在训练时学到了大量的通用知识,当检索到的 chunk 不够用时,它会自动补全——用训练数据里的通用知识填充答案,而且说得非常自信,不会主动告诉你“这部分我是猜的”。在 RAG 场景下,幻觉尤其危险,因为用户以为答案是基于知识库的(可靠的),但实际上模型偷偷掺了自己的私货。
2. RAG 场景下幻觉的三种典型表现
2.1 篡改事实
chunk:iPhone 16 Pro Max 拆封后不支持七天无理由退货。
模型回答:iPhone 16 Pro Max 支持七天无理由退货。
chunk 里明确说了 A,模型回答了 B。这是最严重的一种幻觉。模型不是没看到 chunk 的内容,而是用自己的常识(大部分商品支持七天无理由退货)覆盖了 chunk 的具体规则。
2.2 凭空捏造
chunk:质量问题退货,运费由商家承担。
模型回答:质量问题退货,运费由商家承担,退款将在 3-5 个工作日内原路返回。
3-5 个工作日内原路返回这个信息 chunk 里完全没有,是模型根据电商行业的通用经验编造的。如果实际退款周期是 7-15 个工作日,用户就会投诉“说好的 3-5 天”,为什么还没到账。
2.3 张冠李戴
chunk 1:AirPods Pro 保修期为 1 年。
chunk 2:AppleCare+ 可将保修期延长至 2 年。
模型回答:AirPods Pro 的保修期为 2 年。
chunk A 的信息安到 chunk B 的主体上,模型把两个 chunk 的信息混在一起,生成了一个看起来对但实际上错的答案。这种幻觉特别隐蔽,因为每个单独的信息点都是对的,但组合方式是错的。
通过 Prompt 抑制幻觉的实用技巧
幻觉不能完全消除(这是大模型的本质特性),但可以通过 Prompt 设计大幅降低发生概率。
技巧一:明确限定知识来源
在 System Prompt 中反复强调——只基于参考资料回答。这条指令看起来简单,但效果显著。没有这条指令时,模型会把检索到的 chunk 当作参考;有了这条指令,模型会把 chunk 当作唯一依据。
技巧二:加兜底指令
告诉模型:不知道的时候该怎么办。如果不给这个出口,模型会默认“尽力回答”,也就是开始编造。
如果【参考资料】中没有足够的信息来回答用户的问题,请明确回答:
"根据现有资料,暂时无法回答该问题。建议您联系人工客服获取更多帮助。"
技巧三:禁止编造具体细节
数字、日期、金额这种具体细节是幻觉的重灾区。模型特别喜欢补全这类信息,因为训练数据里有大量类似的模式。
技巧四:要求先引用再回答
让模型在回答时标注引用来源,这不仅提升了可追溯性,还间接抑制了幻觉——因为模型需要为每句话找到对应的 chunk,找不到就不敢说。
回答时请引用参考资料的编号,格式为 [1]、[2] 等,标注在相关句子的末尾。
技巧五:降低Temperature参数
这个需要单独解释一下。
3.1 Temperature 和 Top-P 参数的作用
调用大模型 API 时,有两个参数直接影响生成结果的随机性:
Temperature(温度)
控制模型输出的随机程度。值越低,模型越倾向于选择概率最高的词;值越高,模型越愿意冒险选择概率较低的词。
- Temperature = 0:模型几乎总是选择概率最高的词,输出最确定、最保守
- Temperature = 0.7:默认值,有一定随机性,适合创意写作
- Temperature = 1.0 或更高:输出非常随机,适合头脑风暴
打个比方,Temperature 就像一个人的冒险程度。Temperature = 0 的人回答问题只说最有把握的话;Temperature = 1.0 的人会天马行空,想到什么说什么。
Top-P(核采样)
另一种控制随机性的方式。Top-P = 0.9 表示模型只从累计概率达到 90% 的候选词中采样,排除那些概率极低的长尾选项。
RAG场景下的推荐设置
| 参数 | 推荐值 | 理由 |
|---|---|---|
| Temperature | 0 ~ 0.3 | RAG 需要的是准确性,不是创造性。低 Temperature 让模型更老实 |
| Top-P | 0.9 ~ 0.95 | 配合低 Temperature 使用,进一步减少随机性 |
注意:Temperature 和 Top-P 通常不需要同时调。大多数情况下,只设置 Temperature 就够了。如果你同时设置了两个参数,不同模型的行为可能不一致。建议先只调 Temperature,效果不满意再加 Top-P。
3.2 兜底回答的设计
兜底回答是幻觉抑制的最后一道防线。当检索结果不足以回答用户问题时,模型应该给出一个诚实的不知道,而不是硬编一个答案。
但不知道也有讲究。直接回答我不知道会让用户觉得这个系统没用。好的兜底回答应该做到三点:
- 1.承认信息不足
- 2.给出替代方案(如联系人工客服)
- 3.如果能部分回答,先给出已知的部分
几种兜底回答的模板:
完全无法回答:
根据现有资料,暂时无法回答该问题。建议您联系人工客服(热线:400-xxx-xxxx)获取更多帮助。
部分可以回答:
根据现有资料,关于 [已知部分] 的信息如下:[回答已知部分]。
但关于 [未知部分],现有资料中没有相关说明,建议您联系人工客服确认。
问题超出知识库范围:
这个问题超出了我的服务范围。我主要负责解答商品退换货、物流配送等售后问题。如果您有这方面的疑问,随时可以问我。
引用对齐:让答案可追溯
1. 为什么需要引用对齐
想象一下你在看一篇新闻报道,文章说:据统计,2025 年全球智能手机出货量下降了 15%。你会想:这个数据哪来的?谁统计的?如果文章标注了“来源:IDC 2025 年度报告”,你就能自己去查证。
RAG 系统也是一样。用户看到一个回答,心里的第一反应是这个答案靠谱吗。如果模型能标注“这段话来自《退货政策》第 3 条”,用户就能自己去验证——这比任何我的回答是准确的的声明都有说服力。
在企业级 RAG 系统中,引用对齐更是刚需:
- 合规要求 :金融、医疗、法律等行业,回答必须有据可查
- 审计追溯 :出了问题能追溯到具体的知识来源,定位是知识库的问题还是模型的问题
- 信任建设 :用户能看到答案的依据,逐步建立对系统的信任
2. 引用对齐的实现方式
引用对齐的核心思路很简单:上下文中每个 chunk 带编号,Prompt 中要求模型在回答时标注引用编号。
2.1 Prompt 中的引用指令设计
在 System Prompt 中加入引用指令:
回答时请引用参考资料的编号,格式为 [1]、[2] 等,标注在相关句子的末尾。
如果一句话的信息来自多条参考资料,请同时标注多个编号,如 [1][3]。
只引用你实际使用到的参考资料,不要引用与回答无关的资料。
上下文中的 chunk 格式(前面已经展示过):
[1] 来源:退货政策文档 | 更新时间:2026-01-15
iPhone 16 Pro Max 因屏幕定制工艺,拆封后不支持七天无理由退货。
[2] 来源:通用退货规则 | 更新时间:2026-01-10
标准商品在签收后 7 天内可申请无理由退货,商品需保持完好。
模型的回答:
iPhone 16 Pro Max 拆封后不支持七天无理由退货 [1]。
一般标准商品在签收后 7 天内可以申请无理由退货,但需要商品保持完好 [2]。
2.2 引用解析与展示
模型输出的是带 [1]、[2] 标记的纯文本,后端需要解析这些标记,关联到对应的 chunk 元数据,然后传给前端展示。
解析逻辑很简单——用正则表达式提取 [数字] 模式:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.*;
public class CitationParser {
/**
* 从模型回答中提取所有引用编号
*/
public static Set<Integer> extractCitations(String answer) {
Set<Integer> citations = new TreeSet<>();
Pattern pattern = Pattern.compile("\\[(\\d+)]");
Matcher matcher = pattern.matcher(answer);
while (matcher.find()) {
citations.add(Integer.parseInt(matcher.group(1)));
}
return citations;
}
/**
* 将引用编号替换为可点击的链接(HTML 格式)
*/
public static String renderCitations(String answer, Map<Integer, ChunkMeta> chunkMetaMap) {
Pattern pattern = Pattern.compile("\\[(\\d+)]");
Matcher matcher = pattern.matcher(answer);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
int index = Integer.parseInt(matcher.group(1));
ChunkMeta meta = chunkMetaMap.get(index);
if (meta != null) {
String link = String.format(
"<a href=\"%s\" title=\"%s\" class=\"citation\">[%d]</a>",
meta.getSourceUrl(), meta.getSource(), index
);
matcher.appendReplacement(result, link);
}
}
matcher.appendTail(result);
return result.toString();
}
// chunk 的元数据
static class ChunkMeta {
private String source; // 来源文档名
private String sourceUrl; // 原文链接
private String updateTime; // 更新时间
public ChunkMeta(String source, String sourceUrl, String updateTime) {
this.source = source;
this.sourceUrl = sourceUrl;
this.updateTime = updateTime;
}
public String getSource() { return source; }
public String getSourceUrl() { return sourceUrl; }
public String getUpdateTime() { return updateTime; }
}
}
前端拿到渲染后的 HTML,用户点击 [1] 就能跳转到原文,自己验证答案的准确性。
答案约束:控制输出的格式和边界
Prompt 设计除了抑制幻觉和引用对齐,还有一个重要的维度——控制模型输出的格式和边界。不同的业务场景对答案的形式有不同的要求。
1. 格式约束:JSON / 纯文本 / 列表
纯文本(客服问答场景)
大多数客服场景用自然语言回答就行,这也是默认的输出格式。
请用自然语言回答,语气友好、简洁。
JSON格式(API接口场景)
如果 RAG 系统的输出需要被下游程序消费(比如作为 API 返回值),就需要结构化的 JSON 输出。
请以 JSON 格式回答,包含以下字段:
- answer:回答内容
- citations:引用的参考资料编号列表
- confidence:你对这个回答的确信程度(high / medium / low)
输出示例:
{
"answer": "iPhone 16 Pro Max 拆封后不支持七天无理由退货。",
"citations": [1],
"confidence": "high"
}
注意:要求模型输出 JSON 时,建议在 Prompt 中给一个输出示例。没有示例的话,模型可能会自己发明字段名或者格式不一致。另外,部分大模型 API 支持
response_format参数强制 JSON 输出,比 Prompt 约束更可靠。
列表格式(知识卡片场景)
2. 长度约束:控制回答的详略
不同场景对回答长度的要求差异很大:
# 简短回答(适合快速问答、聊天机器人)
请用 1-2 句话简要回答。
# 详细回答(适合知识库查询、报告生成)
请详细回答,包含所有相关的细节和注意事项。
# 限定字数
请在 200 字以内回答。
经验上,RAG 客服场景推荐中等长度——回答核心问题,补充必要的注意事项,但不要展开无关的背景知识。可以在 System Prompt 中这样写:
回答要简洁但完整:覆盖用户问题的核心要点,补充必要的注意事项,但不要展开与问题无关的背景知识。
3. 边界约束:只回答知识库范围内的问题
用户不一定只问知识库范围内的问题。一个电商客服 RAG 系统,用户可能会问“今天天气怎么样”“帮我写一首诗”“你觉得 iPhone 和华为哪个好”。
如果不做边界约束,模型会用自己的知识回答这些问题——这不仅偏离了系统的定位,还可能产生不当言论(比如品牌对比可能引发争议)。
在 System Prompt 中加入边界约束:
你只负责回答与商品售后、退换货、物流配送相关的问题。
如果用户的问题超出这个范围,请礼貌地告知:"这个问题超出了我的服务范围。我主要负责解答商品售后相关问题,如果您有退换货、物流等方面的疑问,随时可以问我。"
不要回答涉及品牌对比、价格预测、个人观点等主观性问题。
保持了清晰的分组结构(角色与边界、回答规则、引用规则、格式要求)。分组的好处是模型更容易理解每条指令的优先级,也方便你后续针对某个维度做调优。
3. Prompt 模板的迭代优化
Prompt 不是写一版就完事的。上线之后你会不断收到 bad case(用户反馈答案不对),需要根据 bad case 持续调优。
这里给一个实用的迭代流程:
第一步:收集badcase
把用户反馈的错误回答记录下来,至少包含三个信息:用户问题、检索到的 chunk、模型的错误回答。
第二步:分类归因
每个 bad case 归到以下某个类别:
| 类别 | 判断标准 | 优化方向 |
|---|---|---|
| 幻觉——篡改事实 | chunk 说 A,模型说 B | 强化只基于参考资料回答的指令 |
| 幻觉——凭空捏造 | 模型补充了 chunk 里没有的细节 | 强化不要编造的指令,特别是数字、日期、金额 |
| 幻觉——张冠李戴 | 模型混淆了不同 chunk 的信息 | 要求模型逐条引用,不要合并不同来源的信息 |
| 答非所问 | 模型回答了相关但不对口的内容 | 强化聚焦用户问题的指令 |
| 格式不对 | 输出格式不符合要求 | 在 Prompt 中加输出示例 |
| 超出边界 | 模型回答了知识库范围外的问题 | 强化边界约束指令 |
事实上,由于核心原因在于大语言模型(LLM)的数学计算能力和多步逻辑推理能力存在局限性。要求里强调“仅输出标准JSON……不得添加任何额外的解释”时,你实际上剥夺了模型进行“思维链(Chain of Thought)”推导的空间。模型被迫在“大脑”(隐藏层)中一口气完成所有复杂的条件分支和数学计算,这导致它极易算错。而且rag系统的大模型通常是小规模参数模型比如30b(qwen3.5 flash即Qwen3.5-35B-A3B)他默认不给你cot思考模式,要求立刻回答,很难针对具体问题回答,只能泛泛而谈,哎呀阿里的那个笔试题让我写一个prompt,调了1h还是14%通过率真的是我靠的老铁啊
第三步:针对性修改Prompt
举几个真实的 bad case 和对应的 Prompt 修改:
BadCase1:模型把多个chunk的数字混在一起
用户问题:AirPods Pro 的保修期是多久?
chunk 1:AirPods Pro 保修期为 1 年。
chunk 2:AppleCare+ 可将保修期延长至 2 年。
错误回答:AirPods Pro 的保修期为 2 年。
归因:张冠李戴。模型把 AppleCare+ 的延长保修期当成了默认保修期。
Prompt 修改——在回答规则中增加一条:
5. 引用不同参考资料的信息时,请明确区分各条资料的适用条件和主体,不要将不同资料的信息合并表述。
BadCase2:模型在chunk信息不足时自己补了退款时效
用户问题:退货后多久能收到退款?
chunk:质量问题退货,运费由商家承担。
错误回答:退货后 3-5 个工作日内退款到账。
归因:凭空捏造。chunk 里完全没有退款时效的信息,模型用通用知识编了一个。
Prompt 修改——强化兜底指令,让它更具体:
2. 如果【参考资料】中没有足够的信息来回答用户的问题,请明确告知用户哪些部分可以回答、哪些部分资料中没有提及。
示例:"关于退货运费,商家承担质量问题的运费 [1]。但关于退款到账时间,现有资料中没有说明,建议您联系人工客服确认。"
BadCase3:模型回答了与售后无关的问题
用户问题:iPhone 16 Pro Max 拍照效果怎么样?
chunk:(检索返回了 iPhone 16 Pro Max 的退货政策相关 chunk)
错误回答:iPhone 16 Pro Max 搭载了 4800 万像素主摄……(一大段产品介绍)
归因:超出边界。用户问的是产品功能,不是售后问题,但模型用自己的知识回答了。
Prompt 修改——边界约束更明确:
【角色与边界】
- 你只负责回答与商品售后、退换货、物流配送相关的问题。
- 对于商品功能、参数、使用技巧等非售后问题,请回答:"这个问题超出了我的服务范围。我主要负责解答售后相关问题,关于商品功能和参数,建议您查看商品详情页或咨询售前客服。"
Prompt 调优是一个持续的过程,不存在完美的 Prompt。建议建立一个 bad case 库,每次修改 Prompt 后,用所有历史 bad case 回归测试,确保修了新问题没有引入老问题。
1. 代码实现:从检索到生成的完整链路
整体流程分四步:
- 1.从 Milvus 混合检索 + Reranker 重排序拿到 Top-K chunk(这部分上一篇已经实现过,这里直接复用)
- 2.把 Top-K chunk 组装成带编号、带元数据的上下文
- 3.拼接 System Prompt + 上下文 + 用户问题,调用大模型 Chat API
- 4.解析模型回答中的引用编号,关联到 chunk 元数据
完整示例可以查看 TinyRAG 项目 com.nageoffer.ai.tinyrag.retrieve 目录下代码。
先定义几个基础的数据结构:
/**
* 检索到的 chunk,包含内容和元数据
*/@Getter@NoArgsConstructor@AllArgsConstructorpublicclassRetrievedChunk{privateString content;// chunk 文本内容privateString source;// 来源文档名privateString sourceUrl;// 原文链接privateString updateTime;// 更新时间privateDouble score;// 重排序得分}/**
* RAG 生成结果,包含回答文本和引用信息
*/@DatapublicclassRAGResponse{privateString answer;// 模型的回答(原始文本,带 [1][2] 标记)privateString renderedAnswer;// 渲染后的回答(引用标记替换为链接)privateList<CitationInfo> citations;// 引用详情列表/**
* 引用信息
*/@DatapublicstaticclassCitationInfo{privateInteger index;// 引用编号privateString source;// 来源文档privateString sourceUrl;// 原文链接privateString chunkContent;// 被引用的 chunk 内容}}
核心的 RAG 生成服务:
publicclassRAGGenerationService{// SiliconFlow API 配置privatestaticfinalStringAPI_URL="https://api.siliconflow.cn/v1/chat/completions";privatestaticfinalStringAPI_KEY="你的 SiliconFlow API Key";privatestaticfinalStringMODEL="Qwen/Qwen2.5-7B-Instruct";privatestaticfinalOkHttpClient client =newOkHttpClient.Builder().connectTimeout(30,TimeUnit.SECONDS).readTimeout(60,TimeUnit.SECONDS).build();privatestaticfinalGson gson =newGson();/**
* System Prompt 模板——生产级版本
*/privatestaticfinalStringSYSTEM_PROMPT="""
你是一名专业的电商客服助手。你的任务是根据【参考资料】中的信息,准确回答用户的问题。
【角色与边界】
- 你只负责回答与商品售后、退换货、物流配送相关的问题。
- 如果用户的问题超出这个范围,请礼貌地告知用户,并引导回售后相关话题。
- 不要回答涉及品牌对比、价格预测、个人观点等主观性问题。
【回答规则】
1. 只基于【参考资料】中的内容回答问题,不要使用你自己的知识。
2. 如果【参考资料】中没有足够的信息,请明确回答:"根据现有资料,暂时无法回答该问题。建议您联系人工客服获取更多帮助。"
3. 不要编造任何【参考资料】中没有提到的信息,包括数字、日期、金额等。
4. 如果多条参考资料的信息存在冲突,请指出冲突并告知用户以最新的资料为准。
【引用规则】
- 回答时请引用参考资料的编号,格式为 [1]、[2] 等,标注在相关句子的末尾。
- 如果一句话的信息来自多条参考资料,请同时标注多个编号,如 [1][3]。
- 只引用你实际使用到的参考资料。
【格式要求】
- 用简洁、友好的语气回答。
- 回答要覆盖用户问题的核心要点,补充必要的注意事项,但不要展开无关的背景知识。
""";/**
* 步骤一:把检索到的 chunk 列表组装成带编号的上下文
*/publicStringbuildContext(List<RetrievedChunk> chunks){StringBuilder context =newStringBuilder("【参考资料】\n\n");for(int i =0; i < chunks.size(); i++){RetrievedChunk chunk = chunks.get(i);
context.append(String.format("[%d] 来源:%s | 更新时间:%s\n",
i +1, chunk.getSource(), chunk.getUpdateTime()));
context.append(chunk.getContent()).append("\n\n");}return context.toString();}/**
* 步骤二:调用大模型 Chat API
*/publicStringcallLlm(String systemPrompt,String context,String userQuery)throwsIOException{// 拼接完整的用户消息:上下文 + 用户问题String userMessage = context +"【用户问题】\n"+ userQuery;// 构建请求体JsonObject requestBody =newJsonObject();
requestBody.addProperty("model",MODEL);
requestBody.addProperty("temperature",0.1);// 低 Temperature,减少随机性
requestBody.addProperty("max_tokens",1024);JsonArray messages =newJsonArray();// system 消息JsonObject systemMsg =newJsonObject();
systemMsg.addProperty("role","system");
systemMsg.addProperty("content", systemPrompt);
messages.add(systemMsg);// user 消息JsonObject userMsg =newJsonObject();
userMsg.addProperty("role","user");
userMsg.addProperty("content", userMessage);
messages.add(userMsg);
requestBody.add("messages", messages);// 发送请求Request request =newRequest.Builder().url(API_URL).addHeader("Authorization","Bearer "+API_KEY).addHeader("Content-Type","application/json").post(RequestBody.create(
gson.toJson(requestBody),MediaType.parse("application/json"))).build();try(Response response = client.newCall(request).execute()){if(!response.isSuccessful()){thrownewIOException("API 调用失败,状态码:"+ response.code()+",响应:"+ response.body().string());}JsonObject responseJson = gson.fromJson(response.body().string(),JsonObject.class);return responseJson
.getAsJsonArray("choices").get(0).getAsJsonObject().getAsJsonObject("message").get("content").getAsString();}}/**
* 步骤三:解析模型回答中的引用编号
*/publicList<RAGResponse.CitationInfo>parseCitations(String answer,List<RetrievedChunk> chunks){Set<Integer> citedIndexes =newTreeSet<>();Pattern pattern =Pattern.compile("\\[(\\d+)]");Matcher matcher = pattern.matcher(answer);while(matcher.find()){
citedIndexes.add(Integer.parseInt(matcher.group(1)));}List<RAGResponse.CitationInfo> citations =newArrayList<>();for(int index : citedIndexes){if(index >=1&& index <= chunks.size()){RetrievedChunk chunk = chunks.get(index -1);RAGResponse.CitationInfo info =newRAGResponse.CitationInfo();
info.setIndex(index);
info.setSource(chunk.getSource());
info.setSourceUrl(chunk.getSourceUrl());
info.setChunkContent(chunk.getContent());
citations.add(info);}}return citations;}/**
* 完整的 RAG 生成流程:检索 → 组装 → 生成 → 解析
*
* @param chunks 经过混合检索 + 重排序后的 Top-K chunk
* @param userQuery 用户的原始问题
* @return 包含回答和引用信息的 RAGResponse
*/publicRAGResponsegenerate(List<RetrievedChunk> chunks,String userQuery)throwsIOException{// 1. 组装上下文String context =buildContext(chunks);// 2. 调用大模型String answer =callLlm(SYSTEM_PROMPT, context, userQuery);// 3. 解析引用List<RAGResponse.CitationInfo> citations =parseCitations(answer, chunks);// 4. 组装结果RAGResponse response =newRAGResponse();
response.setAnswer(answer);
response.setCitations(citations);return response;}}
几个值得注意的设计点:
- SystemPrompt和上下文分开传递 :System Prompt 放在
role: system消息里,上下文和用户问题放在role: user消息里。这样做的好处是模型能更清楚地区分"行为指令"和"参考内容",效果比全部塞在一个消息里要好 - Temperature设为0.1 :不设 0 是因为完全确定性的输出有时候会导致回答过于生硬,0.1 保留了一点点灵活性,同时几乎不会产生幻觉
- 引用解析做了边界检查 :
index >= 1 && index <= chunks.size(),防止模型输出了不存在的引用编号(比如只有 3 个 chunk,模型却引用了 [5])
2. 运行效果展示
用一个完整的例子跑一遍。假设用户问的是"iPhone 16 Pro Max 拆封后还能退吗?运费谁出?",经过混合检索 + 重排序,拿到了 3 个 chunk:
publicclassRAGDemo{publicstaticvoidmain(String[] args)throwsException{RAGGenerationService service =newRAGGenerationService();// 模拟检索 + 重排序后的 Top-3 chunkList<RetrievedChunk> chunks =List.of(newRetrievedChunk("iPhone 16 Pro Max 因屏幕定制工艺,拆封后不支持七天无理由退货。如需退货,需经售后检测确认存在质量问题。","退货政策文档","/docs/return-policy","2026-01-15",0.95),newRetrievedChunk("标准商品在签收后 7 天内可申请无理由退货,商品需保持完好,不影响二次销售。","通用退货规则","/docs/general-return","2026-01-10",0.82),newRetrievedChunk("质量问题退货,运费由商家承担;非质量问题退货,运费由买家承担。","退货运费规则","/docs/return-shipping","2026-02-01",0.78));String userQuery ="iPhone 16 Pro Max 拆封后还能退吗?运费谁出?";// 执行 RAG 生成RAGResponse response = service.generate(chunks, userQuery);// 输出结果System.out.println("=== 用户问题 ===");System.out.println(userQuery);System.out.println();System.out.println("=== 模型回答 ===");System.out.println(response.getAnswer());System.out.println();System.out.println("=== 引用来源 ===");for(RAGResponse.CitationInfo citation : response.getCitations()){System.out.printf("[%d] %s(%s)%n",
citation.getIndex(), citation.getSource(), citation.getSourceUrl());}}}
运行输出:
=== 用户问题 ===
iPhone 16 Pro Max 拆封后还能退吗?运费谁出?
=== 模型回答 ===
iPhone 16 Pro Max 因屏幕定制工艺,拆封后不支持七天无理由退货。如果需要退货,需经售后检测确认存在质量问题,此时运费由商家承担 [1]。对于其他标准商品,签收后 7 天内可申请无理由退货,商品需保持完好,不影响二次销售,此时运费由买家承担 [2][3]。
=== 引用来源 ===
[1] 退货政策文档(/docs/return-policy)
[2] 通用退货规则(/docs/general-return)
[3] 退货运费规则(/docs/return-shipping)


Comments NOTHING