1. 问题背景:为什么一个简单的Prompt让我的RAG系统“翻车”
上个月我负责的智能客服项目进入联调阶段,基于LlamaIndex构建的RAG管线在内部测试集上的准确率只有惨不忍睹的7.2%。所有检索到的上下文都是正确的,但生成的答案要么答非所问,要么直接输出“我不知道”。排查了Embedding模型、Chunk大小、检索TopK之后,我发现问题出在最容易被忽视的环节——Prompt。
当时我的Prompt只有一句话:“Answer the question based on the context.” 这种零样本Prompt在简单任务上尚可,但在需要多跳推理和知识融合的客服场景中,GPT-4o(版本gpt-4o-2024-05-13)完全“放飞自我”,经常忽略上下文中的关键约束,生成幻觉内容。
2. 环境与版本:一套可复现的评测基础设施
为了严谨对比,我固定了除Prompt以外的所有参数:
- 模型:
gpt-4o-2024-05-13,temperature=0.2(降低随机性),max_tokens=512 - 向量库:
ChromaDB0.4.24,Embedding模型text-embedding-3-small(维度1536) - 框架:
LlamaIndex0.10.43 +LangChain0.2.5(用于评测编排) - 数据集:自建客服QA集,包含200条问题,覆盖订单查询、退换货、物流异常3类场景,每条问题附带5段检索上下文(由
SimilarityPostprocessor过滤后得到) - 评测指标:完全匹配率(Exact Match,EM)+ 语义相似度(基于
text-embedding-3-large的余弦阈值0.85判定通过)
基线Prompt定义为:"Answer the question based on the context.\nContext:\n{context}\nQuestion:\n{question}\nAnswer:" 实测EM为7.2%(200条中仅14条完全正确),且平均Token消耗2431(因为模型经常复述上下文)。
3. 方案设计:六轮Prompt迭代的思考路径
我设计了6个递进式Prompt模板,分别代表不同的工程策略:
| 版本 | 策略 | 核心改动 |
|---|---|---|
| V1 | 零样本(基线) | 无任何约束 |
| V2 | 角色限定 | 增加“你是客服专家”角色前缀 |
| V3 | 输出格式约束 | 强制JSON输出 + 字段说明 |
| V4 | 少样本示例 | 加入2个场景的问答对 |
| V5 | Chain-of-Thought | 要求“先分析再回答” |
| V6 | 组合策略 | 角色+格式+动态示例(基于检索相似度动态选择示例) |
设计原则:每次只改动一个变量,避免混淆因素。V6的核心思路是“动态少样本”——从示例库中检索与当前问题最相似的2个示例注入Prompt,这样既保留了少样本的指导性,又避免固定示例在多场景下的负迁移。
4. 核心实现:基于LangChain的Prompt评测框架
我写了一个通用的评测脚本,核心是PromptTemplate的替换和结果采集。下面是V6的动态少样本Prompt构造代码:
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
import numpy as np
from openai import OpenAI
# 初始化Embedding模型用于动态示例检索
embed_client = OpenAI(api_key="sk-xxx")
example_pool = [
{"question": "我的订单显示已签收但没收到货",
"answer": "请先核实物流单号,若显示签收但未收到,建议联系快递网点确认代收点,或提交物流异常工单。"},
{"question": "退换货运费谁承担",
"answer": "若商品质量问题,运费由商家承担;若七天无理由退换,买家承担运费,具体以售后政策为准。"},
# ... 更多示例
]
def build_dynamic_prompt(question, context, k=2):
# 计算问题与示例库的相似度,选择最相似的k个示例
q_emb = embed_client.embeddings.create(model="text-embedding-3-small", input=question).data[0].embedding
scores = []
for ex in example_pool:
ex_emb = embed_client.embeddings.create(model="text-embedding-3-small", input=ex["question"]).data[0].embedding
scores.append(np.dot(q_emb, ex_emb) / (np.linalg.norm(q_emb) * np.linalg.norm(ex_emb)))
top_indices = np.argsort(scores)[-k:][::-1]
examples_text = "\n".join(
[f"示例{i+1}\n问题:{example_pool[idx]['question']}\n答案:{example_pool[idx]['answer']}"
for i, idx in enumerate(top_indices)]
)
template = """你是一位资深客服专家,请基于给定的上下文准确回答用户问题。
遵循以下要求:
1. 如果上下文中没有答案,直接回复“基于当前信息无法回答”,不要编造。
2. 回答必须简洁,不超过50字。
3. 先分析关键信息,再给出最终答案。
参考示例(注意示例仅为风格参考,内容以上下文为准):
{examples}
上下文:
{context}
用户问题:{question}
请按以下JSON格式输出:
{{"analysis": "简要分析", "answer": "最终答案"}}"""
return template.format(examples=examples_text, context=context, question=question)
# 评测循环
llm = ChatOpenAI(model="gpt-4o-2024-05-13", temperature=0.2)
for q in test_set:
prompt = build_dynamic_prompt(q.question, q.context)
response = llm.invoke([HumanMessage(content=prompt)])
# 解析JSON并计算EM
V5的Chain-of-Thought模板核心区别在于要求模型输出“思考链”:
cot_template = """请逐步思考以下问题,并在标签中展示推理过程,最后在标签中给出结论。
上下文:{context}
问题:{question}
注意:如果上下文不足,请明确说明缺失信息。"""
5. 踩坑与优化:Token消耗的“隐形杀手”
实验过程中有三个坑值得大家注意:
坑1:示例污染答案。 V4固定示例时,发现模型会“抄袭”示例的句式甚至内容。比如示例中回答“运费由买家承担”,当新问题是“质量问题运费谁出”时,模型直接复制示例答案。解决办法是在示例前加提示语“内容以上下文为准,示例仅为风格参考”。
坑2:Chain-of-Thought导致Token爆炸。 V5的EM虽然提升到68.5%,但平均Token消耗飙升至3890,因为模型输出了大段推理过程。最终V6中我限制了analysis字段不超过30字,将Token压回1187。
坑3:JSON输出时的格式漂移。 当模型偶尔输出``json代码块时,json.loads会报错。我在解析层增加了容错处理——用正则提取第一个{到最后一个}`之间的内容。
优化后的V6模板中,我额外在SystemMessage中注入了业务规则(如“不承诺赔偿金额”),这使EM进一步提升了3.2个百分点。
6. 效果数据:Token消耗与准确率的权衡曲线
最终评测结果(200条测试集,temperature=0.2,重复3次取均值):
| 版本 | EM准确率 | 语义通过率 | 平均Token/查询 | 平均延迟(s) |
|---|---|---|---|---|
| V1基线 | 7.2% | 12.5% | 2431 | 2.8 |
| V2角色 | 21.0% | 30.0% | 2256 | 2.6 |
| V3格式 | 33.5% | 44.5% | 1987 | 2.4 |
| V4少样本 | 52.0% | 61.0% | 2104 | 2.5 |
| V5 CoT | 68.5% | 76.5% | 3890 | 4.2 |
| V6组合 | 91.4% | 94.0% | 1187 | 1.65 |
有趣的是,V6的Token消耗比V5低69.5%,因为动态示例比冗长的思考链更高效,且输出格式约束强制模型精简。延迟从2.8s降至1.65s(主要得益于Token生成量减少)。
附:V6最终Prompt模板(简化版)
System: 你是资深客服专家,禁止编造信息。若上下文不足,回复“信息不足”。
User: 请基于上下文回答问题,输出JSON{"analysis":"<=30字","answer":"<=50字"}。
参考示例1:{动态检索}
参考示例2:{动态检索}
上下文:{context}
问题:{question}
7. 总结:Prompt工程不是玄学,是结构化实验
这次调优最大的收获是:Prompt的边际收益远大于微调模型。在固定模型和检索链路的前提下,仅通过Prompt迭代就将准确率提升了12.7倍,同时降低了51%的Token消耗。几个可复用的经验:
- 角色限定是性价比最高的单项优化(+13.8% EM,零Token成本)
- 输出格式约束是Token杀手,但必须配合解析容错
- 动态少样本优于固定少样本,尤其适用于多场景混合的RAG系统
- CoT慎用,在客服场景中思考链会引入过多噪声,除非任务需要多跳推理
最后提醒大家:Prompt评测一定要用固定测试集+多次采样取均值,否则单次运行的随机性会误导你的判断。如果各位有更好的Prompt策略,欢迎在评论区交流。