提示词工程实战:让 RAG 答疑机器人从“能答“到“答得好“
提示词工程实战:让 RAG 答疑机器人从"能答"到"答得好"
上一节用 RAG 给答疑机器人补上了公司私有知识,解决了"不知道"的问题。但跑起来之后很快会发现新的不对劲:回答是对的,却不好用——该给链接的没给,格式随模型心情,语气像一份冷冰冰的公告。这一篇记录把机器人从"能答"打磨到"答得好"的完整过程:从一次失败的"补丁式"改造讲起,到提示词框架与模板,再到 Meta Prompting、参考答案驱动的自动迭代,最后训练一个"AI 裁判"来给整个优化流程踩刹车。
一、先看一个失败的"补丁"
场景很简单:新同事问机器人"公司内容管理工程师做项目该用什么工具"。机器人靠 RAG 检索回答了 Jira 和 Trello,内容没错。但同事反馈:要是能直接附上官网链接就好了。
最直觉的改法是什么?在用户问题后面手动拼一句指令:
# ch03/patch_demo.py
question = "我们公司项目管理应该用什么工具?"
instruction = " 回答时请务必附带上工具的官方网站或下载链接。"
# 把问题和指令直接拼在一起发给模型
new_question = question + instruction
这个补丁在工具类问题上确实有效,模型老老实实附上了 Jira 和 Trello 的官网。但换个问题立刻翻车。同事接着问"公司年假有多少天",同一条指令拼上去,模型的回答变成了:
至于您要求的工具官方网站或下载链接,由于您的问题并未提及需要使用哪个工具,因此无法提供相关链接。
一个问年假的问题,被强行塞了段关于工具链接的解释。问题的根源在于:指令是静态的,问题是动态的。每次提问都手动判断该拼哪句指令,在真实应用里根本行不通。正确做法是把"什么时候该做什么"写进机器人自己的系统提示词里,让模型自己判断。这就是提示词工程的起点。
二、提示词框架与模板
六个基本要素
写提示词和向人交代工作是一回事:需求说得越清楚,执行越不走样。一个常用的框架包含六个要素:
| 要素 | 作用 |
|---|---|
| 任务目标(Object) | 明确要求模型完成什么,让它专注具体目标 |
| 上下文(Context) | 交代背景信息、操作流程,框定讨论范围 |
| 角色(Role) | 模型扮演的身份,影响语气与专业深度 |
| 受众(Audience) | 回答给谁看,约束内容的深浅和用词 |
| 样例(Sample) | 给出参考案例,模型会从中模仿格式与风格 |
| 输出格式(Output Format) | 规定输出的结构和类型,必要时写明"不要输出什么" |
不必每次都用全六个,但每一次优化提示词时过一遍这张清单,能少漏掉很多东西。
用模板替换 LlamaIndex 的默认提示词
RAG 应用里的提示词并不是硬编码在模型里的,而是一段带占位符的模板。LlamaIndex 的默认问答模板长这样:
Context information is below.
---------------------
{context_str}
---------------------
Given the context information and not prior knowledge, answer the query.
Query: {query_str}
Answer:
检索器会把检索到的资料填进 {context_str},把用户问题填进 {query_str}。这是个通用模板,管不住机器人的行为——它不知道自己叫"公司小蜜",也不知道工具类问题要附链接。用 PromptTemplate 写一份专属模板并注入查询引擎:
# rag_app/prompt_template.py
from llama_index.core import PromptTemplate
def update_prompt_template(
query_engine,
qa_prompt_tmpl_str=(
"你叫公司小蜜,是公司的答疑机器人。"
"你需要仔细阅读参考信息,然后回答大家提出的问题。"
"注意事项:\n"
"1. 根据上下文信息而非先验知识来回答问题。\n"
"2. 如果是工具咨询类问题,请务必给出下载地址链接。\n"
"3. 如果是员工部门查询问题,请务必注意存在同名员工的情况,"
"可能有2个、3个甚至更多同名的人。\n"
"以下是参考信息。"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"问题:{query_str}。\n"
"回答:"
)):
qa_prompt_tmpl = PromptTemplate(qa_prompt_tmpl_str)
# update_prompts 的键是 LlamaIndex 内部约定:
# "response_synthesizer:text_qa_template" 覆盖默认的问答模板
query_engine.update_prompts(
{"response_synthesizer:text_qa_template": qa_prompt_tmpl}
)
return query_engine
注意第三条注意事项——它来自真实的踩坑:检索出的部门信息里有两个同名员工,机器人只报了一个人,被用户投诉"信息不对"。把这类业务规则写进模板,等于给机器人立了规矩。改完之后,问"项目管理用什么工具",机器人会主动附上官网链接;问其他问题也不会被无关指令干扰。这一步把第一节那个失败的"补丁"变成了系统级的正确方案。
三、四个立刻能用的提示词技巧
用分隔符划清边界
当提示词里既有任务要求又有待处理的文本时,用符号把不同部分隔开,模型能更准地识别哪部分是指令、哪部分是数据。常用的有 【】、###、===、---,或者 XML 标签 <tag></tag>:
# ch02/test06_separator.py
question = """
把下列【】括起来的文本进行扩写和润色,让文案生动且富有创造力,并且受到公司新员工的喜欢。
【新员工训练营活动】
"""
一个细节:如果提示词正文里已经在大量使用某种符号(比如满篇都是【】),就不要再拿它当分隔符,否则隔了个寂寞。
限定角色和受众
同一个问题"Qwen-VL 是什么",把模板里的角色从"资深大模型算法工程师"换成"小学老师",输出风格天差地别:前者讲多模态预训练、图像标注、视觉问答;后者讲"它有一个聪明的’大脑’,能看懂小猫玩毛线球"。角色决定语气和专业度,受众决定内容深浅。这两个要素分别填的是"你是谁"和"写给谁",缺一不可。
规定输出格式
想让模型输出接进下游系统,就得让它输出结构化数据。直接在提示词里规定格式即可:
# ch02/test08_output.py
question_task = """
【任务要求】
你将看到一句话或一段话。你需要审查这段话中有没有错别字。
如果出现了错别字,你要指出错误,并给出解释。
"的" 和 "地" 混淆不算错别字
---
【输出要求】
请你只输出json格式,不要输出代码段
其中,label只能取0或1,0代表有错误,1代表没有错误
reason是错误的原因
correct是修正后的文档内容
---
【用户输入】
以下是用户输入,请审阅:
"""
question_doc = "分隔符是特殊的符号,它们帮助大语言模形 (LLM) 识别提示中哪些部分应当被视为一个完整的意思单元。"
question = question_task + question_doc
模型会稳定输出这样的 JSON:
{
"label": 0,
"reason": "存在错别字:'模形'应为'模型'。",
"correct": "分隔符是特殊的符号,它们帮助大语言模型 (LLM) 识别提示中哪些部分应当被视为一个完整的意思单元。"
}
这种能力是很多"AI 接入老系统"方案的基石:模型负责判断,JSON 负责传输,老代码只管解析。
提供少样本示例
规定格式能保证结构对,但风格是否统一还得看样例。比如想让生成的教程固定为"主题、材料清单、步骤、结束语"四段式,光靠描述很难说清,不如直接塞一个范例进去,让模型照着模仿。经验是:格式用文字描述,风格用样例示范,两者配合输出的稳定性最高。
另外,对数学计算这类多步推理任务,还有一招:在提示词里要求"一步步推导"。直接要答案,模型容易拍脑袋算错;要求它写出中间步骤,准确率明显上升。这就是思维链(CoT)的朴素形态。
四、Meta Prompting:让大模型当你的提示词教练
手工迭代提示词的流程每个人都很熟:写一版、跑一下、看哪里不满意、改一版、再跑。这个循环有效但费人。既然模型这么能干,能不能让它自己来干这活?
这就是 Meta Prompting:把不满意的提示词和它生成的回答一起交给模型,让它以"提示词工程专家"的身份帮你重写。
为了让后续代码可以独立运行,先准备一个最小的大模型客户端(文中所有示例都调用它,替代课程项目里私有的工具模块):
# rag_app/llm_client.py
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
def llm_ask(prompt, model="qwen-plus"):
"""发送一段提示词,返回模型的文本回复"""
completion = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}]
)
return completion.choices[0].message.content
然后构造"教练提示词"。关键在于把期望说得足够具体——语气要什么、结构要什么、内容怎么分类,一条条列出来:
# ch03/test02_meta_prompt.py
from rag_app.llm_client import llm_ask
def get_optimization_prompt(initial_prompt, response):
meta_prompt = f"""
我正在为公司的新员工答疑机器人优化一个提示词,
目标是回答关于"公司福利"的问题。
这是我的第一个尝试:
---
{initial_prompt}
---
这是它生成的输出:
---
{response}
---
这个输出不够好。我希望机器人的回答更具吸引力,并且结构清晰,
能让新员工快速抓住重点。具体要求如下:
1. 语气:友好、热情,有欢迎新同事的感觉。
2. 结构:使用清晰的要点(比如用表情符号开头的列表)来组织内容。
3. 内容:将福利分为几个类别,如"健康与假期"、"补贴与激励"等。
请你扮演一位提示词工程专家,帮我重写这个提示词,以实现上述目标。
"""
return llm_ask(meta_prompt)
if __name__ == '__main__':
initial_prompt = """
根据以下信息,回答新员工关于公司福利的问题。
【参考信息】
(此处为检索到的福利政策文本……)
"""
# 先用初始提示词跑一版,拿到不满意的回答
response = llm_ask(initial_prompt)
# 再让 AI 教练重写提示词
optimized = get_optimization_prompt(initial_prompt, response)
# 用新提示词重新生成
print(llm_ask(optimized))
实测下来,"教练"给出的新提示词几乎会自觉用上前面讲的所有技巧:明确角色、清晰任务描述、规定输出格式。也就是说,提示词工程的这些套路不是玄学,模型自己就懂——只是需要有人(或另一个提示词)把这些要求组织起来。
五、参考答案 + 差距分析:把优化变成自动循环
上一步有个软肋:对"教练"说的是"友好"“结构清晰"这类定性目标,它理解得准不准全看运气。想更可控,就升级成"量化对齐”——直接给它一份心目中的满分参考答案,让迭代变成"不断缩小与参考答案的差距"。
整个循环分四步:生成回答 → 让"评审员"模型对比参考答案输出差距报告 → 把报告连同旧提示词交给"优化器"模型重写 → 用新提示词进入下一轮,直到报告里出现"差距很小"。
# ch03/test03_iter_optimize.py
from rag_app.llm_client import llm_ask
# 1. 设定参考答案(由人撰写,或用最强的模型生成后人工把关)
reference_answer = """
👋 欢迎加入我们的大家庭!很高兴能为你介绍我们超棒的福利政策:
**🏥 健康与假期,我们为你保驾护航:**
- **全面健康保险**:覆盖你和你的家人,安心工作无烦忧。
- **带薪年假**:每年足足15天,去探索诗和远方吧!
……
**💰 补贴与激励,为你加油打气:**
- **交通补贴**:每月500元,通勤路上更轻松。
……
"""
# 2. 差距分析
def gap_analyze(generated_answer, reference):
gap_analysis_prompt = f"""
【角色】你是一位文本比较专家。
【任务】请详细比较【生成回答】与【参考答案】之间的差距。
【参考答案】
{reference}
---
【生成回答】
{generated_answer}
---
【要求】
请从语气、结构、内容细节、格式(如表情符号使用)等方面,
输出一份详细的差距分析报告。如果两者几乎没有差距,请直接回答"差距很小"。
"""
return llm_ask(gap_analysis_prompt)
# 3. 根据差距报告优化提示词
def optimize_prompt_with_gap_analysis(current_prompt, gen_response, gap_report):
optimization_prompt = f"""
【角色】你是一位顶级的提示词工程师。
【任务】根据提供的"差距分析报告",优化"当前提示词",
使其能够生成更接近"参考答案"的输出。
---
【当前提示词】
{current_prompt}
---
【生成回答】
{gen_response}
---
【差距分析报告】
{gap_report}
---
【要求】
请只返回优化后的新提示词,不要包含任何其他解释。
"""
return llm_ask(optimization_prompt)
if __name__ == '__main__':
current_prompt = "根据以下信息,回答新员工关于公司福利的问题。……"
for i in range(3):
print(f"--- 第 {i + 1} 轮迭代 ---")
current_response = llm_ask(current_prompt)
gap_report = gap_analyze(current_response, reference_answer)
if "差距很小" in gap_report:
print("评估通过,优化完成!")
break
current_prompt = optimize_prompt_with_gap_analysis(
current_prompt, current_response, gap_report)
else:
print("达到最大迭代次数,停止优化。")
几个工程细节值得注意:
- 迭代必须设最大轮数上限,"差距很小"的判断只是正常出口,上限才是保底。
for...else在这里用得很妙:循环正常跑完(没有 break)时输出"达到最大迭代次数",把停止原因和循环结构绑在一起,不需要额外的标志变量。- 参考答案是整个流程的"靶子",它的质量决定优化的天花板。垃圾参考答案只会把提示词越调越歪。
六、训练一个"AI 裁判"来踩刹车
差距分析还有个隐患:每轮都要调用大模型写一份"差距报告",判断停止与否靠的是报告里有没有"差距很小"四个字——这既是成本,也是不严谨。更工程化的做法是先训练一个只输出"通过 / 不通过"的 AI 裁判,再让它驾驶整个优化循环。
训练裁判的思路完全照搬机器学习:裁判提示词就是"模型",标注样本就是"数据集",改错题就是"训练"。
准备数据集并拆分
# ch03/test05_ai_judge.py
from rag_app.llm_client import llm_ask
# 1. 准备标注样本:每条包含回答文本和人工标注的结论
labeled_samples = [
{
"id": "train_01",
"response": "公司福利:健康保险,家属可用。15天年假,5天病假。"
"交通补贴500,餐饮补贴300。培训基金8000。健身房有折扣。",
"label": "不通过",
},
{
"id": "train_03",
"response": "👋 欢迎加入!我们为你准备了超棒的福利:\n"
"- 🏥 全面健康保险和年度体检\n"
"- 🌴 15天年假+5天病假\n"
"- 💰 每月交通和餐饮补贴\n"
"- 🎓 高达8000元的培训基金\n期待与你共事!🎉",
"label": "通过",
},
# ……更多样本
]
# 按比例拆分训练集和评估集
# 实际项目里可以用 sklearn 的 train_test_split 随机拆
train_set = labeled_samples[:3]
eval_set = labeled_samples[3:]
为什么要分训练集和评估集?因为裁判提示词也可能"过拟合":它把训练题背得滚瓜烂熟,换个新题就不会判了。监控两个分数的差值,是判断过拟合最直接的办法。
迭代训练:从错题里学
# ch03/test05_ai_judge.py(续)
# 2. 评估函数:在指定数据集上计算裁判的准确率,收集错题
def eval_judge_prompt(judge_prompt, dataset):
correct_predictions = 0
misjudged_cases = []
for sample in dataset:
prompt = judge_prompt.format(response_to_judge=sample["response"])
result = llm_ask(prompt).strip().replace("。", "")
if result == sample["label"]:
correct_predictions += 1
else:
misjudged_cases.append({
"response": sample["response"],
"current_judgment": result,
"correct_judgment": sample["label"],
})
accuracy = correct_predictions / len(dataset)
return accuracy, misjudged_cases
# 3. 训练循环:v1 裁判 -> 判错 -> 错题喂给优化器 -> v2 裁判
if __name__ == '__main__':
judge_prompt = """
【角色】你是一位员工体验专家。
【任务】判断【待评估的回答】是否合格。
【评判标准】
1. 结构清晰: 信息分点或分类列出。
2. 信息完整: 提及核心福利。
【待评估的回答】
---
{response_to_judge}
---
【输出要求】请只回答"通过"或"不通过"。
"""
max_iterations = 5
for i in range(max_iterations):
train_accuracy, train_misjudged = eval_judge_prompt(judge_prompt, train_set)
eval_accuracy, eval_misjudged = eval_judge_prompt(judge_prompt, eval_set)
print(f"--- 第 {i + 1} 轮迭代 ---")
print(f"训练集准确率: {train_accuracy:.0%}")
print(f"评估集准确率: {eval_accuracy:.0%}")
if not eval_misjudged:
print("评估集上已无错误,训练完成。")
break
# 取一条错题,连同"我认为的错因"一起交给优化器
case = eval_misjudged[0]
optimizer_template = """
【角色】你是一位顶级的提示词工程师。
【背景】我当前的裁判提示词在评估一个样本时犯了错误。
【我当前的裁判提示词】
---
{current_judge_prompt}
---
【发生错误的案例】
- 待评估的回答: "{error_response}"
- 我的工具的错误判断: "{error_current_judgment}"
- 我期望的正确判断: "{error_correct_judgment}"
- 我认为出错的原因是:当前标准太松,没有强调'热情友好'的语气。
【任务】请重写我的裁判提示词,使其标准更严格,能修正上述错误。
【要求】请只返回优化后的新提示词。
"""
filled = optimizer_template.format(
current_judge_prompt=judge_prompt,
error_response=case["response"],
error_current_judgment=case["current_judgment"],
error_correct_judgment=case["correct_judgment"],
)
judge_prompt = llm_ask(filled)
跑下来的过程很有说服力:v1 裁判标准只写了"结构清晰、信息完整",于是一条分了点但语气冷淡的回答被判成"通过"——错题暴露了标准太松;优化器生成 v2,加上"必须有明确的、热情的欢迎语"这条硬标准,训练集和评估集的准确率都到了 100%。
裁判驾驶优化循环
裁判训练好之后,替换掉第五节里"差距报告"那个环节,整个流程就闭环了:
生成回答 -> AI 裁判判决
|-- 通过 -> 优化结束,采纳当前提示词
|-- 不通过 -> 把"失败的回答 + 裁判的规则书"交给优化器重写提示词 -> 下一轮
一个必须写对的小细节在判决判断上:
| 现象 | 真相 |
|---|---|
| 裁判回答明明是"不通过",代码却判定通过了 | 用 "通过" in judgment 做判断,而"不通过"字符串里也包含"通过"子串 |
| 模型偶尔输出"通过。"(带句号)导致比较失败 | 模型输出不是严格的枚举,先 strip() 并去掉句号,再用 == 精确比较 |
七、量化评测:把"感觉更好"变成数字
提示词迭代到第几版更好?靠肉眼比会陷入"公说公有理"。可以让大模型当"评分员",按固定维度打 1-5 分,再把三个版本的回答得分画成图:
# ch03/test04_gender.py
import json
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Matplotlib 中文字体设置,'SimHei'(黑体)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def grade_response_detailed(response_to_grade):
grader_prompt = f"""
【角色】你是一位经验丰富的内部沟通和员工体验评测官。
【任务】请根据以下四个维度,对提供的文本进行1-5分的量化评分。
【评分维度】
1. 欢迎语气 (welcoming_tone)
2. 内容结构化 (structuring)
3. 视觉吸引力 (visual_appeal)
4. 信息完整性 (completeness)
【待评估文本】
{response_to_grade}
---
【输出要求】
请严格以JSON格式返回你的评分,不要包含任何解释。例如:
{{"welcoming_tone": 5, "structuring": 4, "visual_appeal": 5, "completeness": 5}}
"""
raw_output = llm_ask(grader_prompt)
# 模型偶尔会在 JSON 前后带多余文字,截取第一个 { 到最后一个 }
json_str = raw_output[raw_output.find('{'):raw_output.rfind('}') + 1]
try:
return json.loads(json_str)
except json.JSONDecodeError:
# 容错:解析失败返回最低分默认值,避免程序崩溃
return {"welcoming_tone": 1, "structuring": 1,
"visual_appeal": 1, "completeness": 1}
if __name__ == '__main__':
scores = {
"Original": grade_response_detailed(poor_response),
"Single Optimize": grade_response_detailed(medium_response),
"Multi Optimize": grade_response_detailed(good_response),
}
# 宽表转长表,适配 seaborn 的分组柱状图
df = pd.DataFrame(scores).reset_index().rename(columns={'index': 'Dim'})
df_long = df.melt(id_vars='Dim', var_name='Version', value_name='Score')
ax = sns.barplot(data=df_long, x="Dim", y="Score",
hue="Version", palette="viridis")
ax.set_ylim(0, 6)
plt.legend()
plt.tight_layout()
plt.show()
图表结果符合预期:未优化版在四个维度全面垫底,单轮优化版结构上去了但语气仍平淡,多轮迭代版各项接近满分。这其实就是第六节那个裁判的"连续打分版"——裁判输出二值结论用于控制流程,评分员输出连续分数用于观测趋势,两者配合刚好。
这里埋着一个非常典型的坑,值得单独拎出来:
| 现象 | 真相 |
|---|---|
| 图表里的中文全部显示成方块 | 中文字体名写错。实测代码里曾把 'SimHei'(黑体)写成 'SimHe',Matplotlib 找不到这个字体就回退到不含中文的默认字体 |
| 模型返回的 JSON 解析失败概率不低 | 模型会自作主张包一层 ```json 代码块或加解释文字,解析前先截取首尾花括号之间的内容,并准备解析失败的兜底逻辑 |
八、通用模型与推理模型怎么分工
上面所有技巧在通用大模型(qwen-plus、DeepSeek-V3 这类)上都适用。还有一类专门为推理设计的模型(如 deepseek-reasoner、qwen3 thinking 系列),输出时会带一段可见的"思考过程",像在草稿纸上一步步演算。
两者不是替代关系,按任务类型分工:
| 维度 | 推理模型 | 通用模型 |
|---|---|---|
| 擅长 | 多步推理、数学计算、代码调试、模糊任务 | 日常问答、文本生成、格式转换 |
| 输出 | 带完整推导链条,慢但可靠 | 简洁直接,快且便宜 |
| 成本 | 时间和费用都更高 | 更低 |
实践中有两条好用的经验:
- 给推理模型的提示词要短而清晰,别画蛇添足。推理模型自己就会深度思考,再写"请一步一步推理"反而可能限制它的发挥;但背景信息、角色、输出格式这些照样要给足。
- 让推理模型当"教练",通用模型当"员工"。Meta Prompting、差距分析、裁判判决这类"元任务"交给推理模型质量更高;而拿着已优化好的提示词做日常生成,通用模型又快又省。
九、把提示词从代码里搬出去
走完全程,回头看这篇做的事:一条静态指令的失败,引出了提示词模板;六个要素和四个技巧,把机器人的回答质量抬上一个台阶;Meta Prompting 和参考答案迭代,把人工调优变成了自动循环;AI 裁判和量化评分,给这个循环装上了可靠的刹车和仪表盘。
最后一条工程建议:提示词是领域知识和工程代码的交界处,实际项目里它往往需要业务专家参与打磨。所以别把长篇提示词硬编码在 .py 文件深处,改成可配置的(独立文件、配置中心,或类似阿里云百炼那样的可视化配置界面),让懂业务的人也能改,工程代码只负责加载和填充变量。提示词是应用里变得最频繁的部分,把它的修改成本降到最低,就是给迭代速度上了保险。
提示词工程的核心不是"写出一句神级咒语",而是搭一套可迭代的系统:模板定行为,样例定风格,参考答案定方向,裁判定标准,评测定数字。当优化过程本身可以被测量、被裁判、被自动化,"答得好"才从一句愿望变成一个工程指标。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)