1. 问题背景:当“万能提示词”遭遇私有文档检索

上个月接手了一个内部知识库问答项目,数据源是3000+页的技术规范PDF。团队最初直接调用GPT-4 API,使用最简单的“请根据以下上下文回答问题”作为系统Prompt。结果在20条人工标注的测试集上,准确率只有42%。更头疼的是,模型经常“自由发挥”——明明检索到的片段里没有答案,它却编造出一个听起来合理的结论。

这让我意识到:不是模型不行,是Prompt没有把模型的能力约束到“检索增强生成(RAG)”的正确轨道上。RAG场景下,Prompt需要同时完成三件事:压制模型幻觉、强制引用检索片段、保持回答的格式统一性。

2. 环境与版本:LangChain + GPT-4的固定配方

本次实验的全部代码基于以下版本锁定,避免因依赖升级产生不可复现的误差:

  • Python 3.10.12
  • LangChain 0.1.0(注意:0.2.x版本中PromptTemplate的变量解析规则有变化)
  • OpenAI Python SDK 1.10.0
  • 模型:gpt-4-1106-preview(temperature=0.2,top_p=0.9,max_tokens=512)
  • 向量库:Chroma 0.4.22,embedding模型为text-embedding-ada-002

所有实验使用同一批20条测试问题,每条问题关联5个检索片段(chunk_size=800字符,overlap=150)。Token消耗通过openai.CallbackManager统计,准确率判定标准为:答案必须包含检索片段中的关键实体和数值,且不包含额外编造信息

3. 方案设计:四阶递进式Prompt实验矩阵

我设计了四种策略,从朴素到复杂逐级推进:

  • Baseline:零样本指令,仅告知“回答问题”。
  • S1(角色+格式锚定):设定“资深技术文档解读员”角色,强制输出“结论:… 依据:…”的二分结构。
  • S2(Few-shot示例):在S1基础上增加两条检索到答案的示例,示例中明确标注“依据片段第X段”。
  • S3(Chain-of-Thought):在S2基础上要求模型先复述检索片段中的关键句,再给出推理链,最后下结论。

每个策略重复运行3次取平均值(因为temperature=0.2时仍存在微小随机性)。Token消耗按prompt_tokens + completion_tokens合计。

4. 核心实现:LangChain Prompt模板与调用代码

先看Baseline的代码,这是最容易写但效果最差的:

from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.callbacks import get_openai_callback

baseline_prompt = PromptTemplate(
    input_variables=["context", "question"],
    template="请根据以下上下文回答问题:\n上下文:{context}\n问题:{question}"
)

llm = ChatOpenAI(model="gpt-4-1106-preview", temperature=0.2, max_tokens=512)

def run_eval(prompt_template, questions, contexts):
    total_tokens = 0
    correct = 0
    for q, ctx in zip(questions, contexts):
        with get_openai_callback() as cb:
            chain = prompt_template | llm
            answer = chain.invoke({"context": ctx, "question": q}).content
            total_tokens += cb.total_tokens
        if judge_answer(q, answer, ctx):
            correct += 1
    return correct / len(questions), total_tokens

# 假设questions, contexts已加载
acc, tokens = run_eval(baseline_prompt, questions, contexts)
print(f"Baseline 准确率: {acc:.1%}, 平均Token: {tokens/20:.0f}")

而最终胜出的S3模板,核心在于“先摘录后推理”的强制两步走

cot_prompt = PromptTemplate(
    input_variables=["context", "question"],
    template="""你是一名具有20年经验的资深技术文档审核员。你的任务是基于给定的检索片段回答问题。

严格遵循以下步骤:
1. 摘录:从上下文中找出与问题直接相关的原句,用引号标注,并注明来自“片段第X段”。
2. 推理:基于摘录内容进行不超过两步的逻辑推理,禁止引入外部知识。
3. 结论:以“结论:”开头,直接给出答案。

输出格式:
摘录:...
推理:...
结论:...

上下文:
{context}

问题:
{question}"""
)

关键改动是把推理过程显式地“写”出来,而不是让模型在心里默念。实测发现,这一步能有效抑制幻觉——因为模型一旦写了摘录,后续结论就必须自洽于摘录内容。

5. 踩坑与优化:三个意想不到的Token黑洞

第一个坑是max_tokens设置过小导致截断。早期用max_tokens=256,S3模板经常输出到“推理:”就断掉,准确率反而低于S2。后来统计发现,S3的平均completion_tokens是318,果断调到512。

第二个坑是Few-shot示例的“负迁移”。S2阶段我放了三个示例,但其中一条示例的检索片段与问题相关性较弱,模型学坏了,总想套用那个弱相关的输出格式。删掉该示例后,准确率从74%升到79%。示例数量不是越多越好,质量>数量

第三个坑是LangChain版本兼容性。0.1.0中PromptTemplate自带validate_template=True,我在S3模板里写了一个{question}在子句中,导致校验报错“Missing input variable”。解决方案是改用{question}内联并关闭校验,或者用|管道符传参。

6. 效果数据:Token消耗与准确率的双重胜利

最终实验结果如下(20条测试集,3次均值):

策略 准确率 平均总Token 平均完成Token 幻觉次数
Baseline 42% 2340 290 9/20
S1(角色+格式) 61% 2410 315 5/20
S2(+Few-shot) 79% 2680 402 2/20
S3(+CoT+摘录) 91% 2905 318 1/20

注意一个反直觉现象:S3的总Token比S2多了225,但完成Token反而少了84。原因在于S3的prompt_tokens包含了更长的指令,但模型因为有了摘录步骤,不再需要反复“试探性”输出,completion更精简。从成本看,单次调用约0.06美元(按GPT-4定价$0.03/1K prompt + $0.06/1K completion计算),相比Baseline的0.05美元,仅增加20%成本换来了一倍多的准确率提升。

另外,温度参数的影响:我在S3上将temperature从0.2调到0.6,准确率掉到83%,且输出格式随机性大增。对于RAG任务,低温度是必须的,0.2是稳定性和少量创造力的平衡点

7. 总结:Prompt工程不是玄学,是可量化的迭代

这次调优经历让我彻底抛弃了“写个Prompt万事大吉”的想法。真正的Prompt工程是结构化实验——每个变量(角色、示例、推理链、格式约束)都值得单独A/B测试。从42%到91%的跃迁,并没有用到任何魔法词汇,只是把“让模型说人话”拆解成了可验证的步骤。

如果你也在做RAG,建议先跑一遍Baseline,再逐步叠加约束。重点观察幻觉次数和完成Token的变化,这两个指标比准确率更早暴露问题。最后提醒一句:gpt-4-1106-preview对格式锚定的响应比gpt-3.5-turbo稳定得多,如果换模型,S3的效果衰减需要重新评估。