4.1 引言:Prompt是智能体的灵魂指令
在前一章里,我们给智能体装上了"大脑"——LLM。但只装大脑还不够。设想一个场景:你新招了一位名校毕业生,智商很高,但你既没告诉他岗位是什么,也没给工作手册,更没说哪些事能做、哪些不能做。他第一天上班会怎样?大概率是手足无措,要么瞎猜乱做,要么啥也不干。
智能体也是一样。LLM本身能力很强,但它不知道自己在这个系统里扮演什么角色、要遵守什么规矩、能调用什么工具。这一切,都需要通过Prompt(提示词)来告诉它。
很多人对Prompt工程有误解,觉得就是"把话说清楚"。但当你真正动手做一个要稳定运行、自主决策、能调用工具的智能体时,你会立刻明白:Prompt是智能体行为的唯一控制面板。同一台GPT-5.4,配上一套粗陋的Prompt,它可能频频胡说八道、动不动拒绝任务、输出格式乱七八糟;换上一套精心打磨的Prompt,它就能稳定输出、推理准确、规规矩矩地调用工具。
打个比方:LLM是一台高性能发动机,Prompt就是行车电脑(ECU)里的那套控制程序。发动机再猛,程序写得稀烂,车照样开不直、拐不准。
💡 什么是Prompt? Prompt就是你写给LLM的"指令文本"。它可以是一句话(“帮我翻译这段话”),也可以是几千字的角色设定+规则+示例。在Agent场景里,Prompt通常很长、很结构化,因为它要承担"指挥官"的角色。
本章会从最基础的System Prompt设计讲起,一路覆盖Few-shot Learning、思维链(CoT)、结构化输出、ReAct模板,最后落地到Prompt的版本管理与A/B测试。每一节都配以可直接运行的Python代码,建议你边读边在本地敲一遍——动手是学编程最快的方式。
💡 本章代码基于OpenAI Python SDK v1.x,模型统一使用gpt-5.4。运行前请先安装依赖,并设置OPENAI_API_KEY环境变量。
1
2
|
# 本章依赖安装
pip install openai pydantic pyyaml
|
4.2 System Prompt设计原则
System Prompt是智能体的"宪法"——它定义了角色、约束、能力边界和输出规范。一个优秀的System Prompt通常包含四个部分:角色定义、任务约束、能力边界、输出格式。
为什么要把这四件事一次性讲清楚?因为LLM是"一次定调、全程跟随"的:System Prompt在对话最开头出现,模型的整个回答风格、判断倾向都受它影响。开头没立好规矩,后面用户再怎么说"别这样",效果都打折扣。这就像新员工入职第一天的"岗前培训"——第一印象定调了,后面很难纠正。
4.2.1 四要素拆解
- 角色定义:告诉模型"你是谁"。不是泛泛的"你是一个助手",而是具体到"你是一个面向中文用户的金融数据分析师"。角色越具体,模型的回答越聚焦。
- 任务约束:明确"做什么、不做什么"。比如"只分析A股市场,不给出投资建议"。
- 能力边界:声明可用工具和知识范围,避免模型越权幻觉(比如明明没工具查实时股价,却编一个数字出来)。
- 输出格式:规定输出的结构、语言、长度,便于下游程序解析。
4.2.2 完整的System Prompt模板
下面是一个面向"客服智能体"的完整System Prompt模板。它采用了Markdown标题分段的方式,便于维护和复用。我们先看模板本身,再看怎么把它接到API调用里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
# system_prompt_template.py
# 客服智能体的System Prompt模板
SYSTEM_PROMPT = """# 角色
你是一名专业的电商客服智能体,名为"小助"。
你服务于一家销售3C数码产品的电商平台,主要面向中文用户。
# 任务
你的职责是解答用户关于订单、物流、退换货、产品参数的问题。
对于你能确定答案的问题,给出清晰、简洁的回复。
对于你无法确定的问题,调用相应工具查询,不要编造答案。
# 约束
1. 只处理3C数码类目相关的问题,其他类目(服饰、食品等)请礼貌引导用户联系对应客服。
2. 不主动推荐商品,不进行价格谈判,不承诺超出政策的补偿。
3. 当用户情绪激动时,先共情再解决问题,语气保持温和、专业。
4. 严禁讨论政治、宗教、种族等敏感话题,遇到此类问题请以"我无法回答此类问题"回应。
# 能力边界
你可以调用以下工具:
- query_order(order_id):查询订单状态
- query_logistics(tracking_no):查询物流轨迹
- submit_refund(order_id, reason):提交退款申请
- search_product(keyword):搜索产品参数
如果你认为需要调用工具,请按照函数调用格式输出;否则直接用自然语言回复。
# 输出格式
- 回复用户时使用中文,语气亲切但专业。
- 每条回复控制在200字以内,复杂问题可用分点列表。
- 涉及金额、时间、单号等关键信息,请用**加粗**标注。
"""
# 使用示例
from openai import OpenAI
client = OpenAI() # 默认从环境变量读取OPENAI_API_KEY
response = client.chat.completions.create(
model="gpt-5.4", # 当前主力模型
messages=[
{"role": "system", "content": SYSTEM_PROMPT}, # 把模板塞进system消息
{"role": "user", "content": "我的订单20250704-001已经三天没动了,帮我看一下"},
],
temperature=0.3, # 客服场景需要稳定,温度调低
)
print(response.choices[0].message.content) # 取出模型回复
|
💡 什么是temperature? 它是控制模型输出"随机性"的参数,范围0到2。0表示几乎每次都给最稳的答案,值越大越"放飞"。客服、分类这种要稳定的场景用0~0.3;写诗、起名字这种要创意的可以用0.7以上。
注意几个细节:第一,System Prompt用Markdown的标题分段,模型对结构化文本的遵循度更高;第二,约束用编号列表而非自然语言段落,便于模型精确匹配;第三,temperature=0.3——客服场景需要稳定可控,不建议用高温度。
⚠️ System Prompt不是越长越好。超过2000 token后,模型对尾部指令的遵循率会下降(人看长文也会走神)。如果Prompt确实很长,建议把最关键的约束放在开头和结尾(首尾效应),中间放次要信息。
4.3 Few-shot Learning在Agent中的应用
4.3.1 原理与适用场景
Few-shot Learning(少样本学习)是指在Prompt中给出少量示例,让模型"照葫芦画瓢"。
生活里我们也有类似经验:你教新同事做表格,与其写一堆"先这样再那样"的文字说明,不如直接给他看三份做好的样表,他模仿着就学会了。Few-shot就是给模型看几份"样表"。
它的底层原理是LLM在预训练阶段形成的模式补全能力:当看到几个输入-输出对时,模型会推断出潜在的映射规则并应用到新输入上。这也是为什么"举例子"比"讲规则"更管用——规则要抽象理解,例子可以直接照搬。
Few-shot在以下场景特别有效:
- 格式控制:需要模型输出特定格式(如表格、JSON)时,示例比规则描述更管用。
- 风格模仿:让模型模仿某种写作风格、对话语气。
- 任务边界澄清:当任务定义模糊时,几个示例能快速对齐模型理解。
- 分类任务:情感分析、意图识别等小样本分类场景。
但Few-shot也有代价:它会占用上下文窗口、增加token成本,且示例选得不好反而会引入偏差。一般3-5个示例就够了,不必堆砌。
4.3.2 代码示例:带Few-shot的意图识别Agent
下面实现一个用户意图识别Agent,通过Few-shot示例让模型把用户的话归到正确的意图类别。这种"意图识别"是客服、助手类Agent的标配——只有先搞懂用户想干嘛,才能路由到正确的处理流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
# few_shot_intent.py
# 基于Few-shot的用户意图识别
from openai import OpenAI
client = OpenAI()
# Few-shot示例:输入-标签对
FEW_SHOT_EXAMPLES = """
用户输入:我想买一台适合打游戏的笔记本,预算8000
意图:product_consult
用户输入:订单20250704-001怎么还没发货
意图:order_query
用户输入:收到的耳机左耳没声音,要退货
意图:refund_request
用户输入:你们这个手机支持多少瓦快充
意图:product_consult
用户输入:物流显示已签收但我没收到
意图:logistics_complaint
"""
def detect_intent(user_input: str) -> str:
"""识别用户意图,返回意图标签"""
system_prompt = f"""你是一个意图分类器。根据用户输入判断其意图,只输出意图标签,不要输出其他内容。
可用意图标签:
- product_consult:产品咨询
- order_query:订单查询
- refund_request:退款/退货
- logistics_complaint:物流投诉
- greeting:问候
- other:其他
以下是几个示例:
{FEW_SHOT_EXAMPLES}
"""
response = client.chat.completions.create(
model="gpt-5.4",
messages=[
{"role": "system", "content": system_prompt}, # 系统消息里塞Few-shot
{"role": "user", "content": user_input}, # 这次要分类的真实输入
],
temperature=0.0, # 分类任务用0温度,保证每次结果一致
)
return response.choices[0].message.content.strip()
# 测试
test_inputs = [
"帮我查一下订单20250704-002发到哪了",
"你好,在吗",
"这个键盘的轴体是什么牌子",
"我要投诉,快递员把包裹扔门口了",
]
for text in test_inputs:
intent = detect_intent(text)
print(f"输入: {text}\n意图: {intent}\n{'-'*40}")
|
运行后你会看到模型准确地把"帮我查一下订单"归为order_query,把"你好,在吗"归为greeting——尽管示例里没有问候语样本,但Few-shot建立起了"输入-标签"的模式,模型能泛化到未见的类别。
💡 选择Few-shot示例时,尽量覆盖不同类别和不同表达方式,避免示例集中在一两类导致模型产生偏置(bias)。比如5个示例全是product_consult,模型就会倾向于把什么都判成product_consult。
4.4 思维链(Chain-of-Thought)
4.4.1 CoT原理与效果
思维链(Chain-of-Thought, CoT)是Prompt工程中最具影响力的技术之一,由Google团队在2022年提出。核心思想是:让模型在给出答案前,先展示推理过程。
为什么CoT有效?回想一下你做数学题的经历:心算容易错,打草稿就稳得多。LLM也是一样。如果直接问"小明有3个苹果,吃了1个,又买了2个,他还剩几个",模型可能跳过推理直接给答案,偶尔会算错。但如果让模型先写出"3-1=2,2+2=4"的过程,每一步都成为后续预测的上下文,准确率会显著提升。
更技术地讲,LLM的本质是"预测下一个token"。一旦模型把"3-1=2"这几个字写出来,这几个字就成了新的上下文,模型预测下一个token时就站在了"3-1=2"这个基础上,比凭空跳到最终答案要稳。
在Agent场景下,CoT的价值更大:智能体需要规划、需要分解任务、需要在调用工具前判断"该不该调用、调用哪个"——这些都是多步推理,没有思维链几乎无法稳定完成。
4.4.2 Zero-shot CoT vs Few-shot CoT
CoT有两种典型用法:
- Zero-shot CoT:不提供示例,只在Prompt中加一句话——“让我们一步一步思考”(Let’s think step by step)。这看似简单,却能在数学、逻辑题上带来10-30个百分点的提升。
- Few-shot CoT:在示例中不仅给出答案,还给出完整的推理过程,让模型模仿这种"先思考后作答"的模式。
下面用一道经典的鸡兔同笼问题对比三种方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
# cot_comparison.py
# 对比Zero-shot CoT与Few-shot CoT
from openai import OpenAI
client = OpenAI()
question = "一个农场有鸡和兔子共35只,脚共94只,问鸡兔各几只?"
# 1. 不使用CoT(基线):直接问,让模型自己决定要不要打草稿
def ask_direct(q: str) -> str:
response = client.chat.completions.create(
model="gpt-5.4",
messages=[{"role": "user", "content": q}],
temperature=0.0,
)
return response.choices[0].message.content
# 2. Zero-shot CoT:只加一句"一步一步思考",不提供示例
def ask_zero_shot_cot(q: str) -> str:
response = client.chat.completions.create(
model="gpt-5.4",
messages=[{"role": "user", "content": f"{q}\n\n请一步一步思考,最后给出答案。"}],
temperature=0.0,
)
return response.choices[0].message.content
# 3. Few-shot CoT:提供一个带完整推理过程的示例
FEW_SHOT_COT = """
示例:
问题:小明有5个苹果,送给小红2个,又从树上摘了3个,现在有几个?
思考过程:
1. 初始:5个
2. 送出2个:5-2=3个
3. 摘了3个:3+3=6个
答案:6个
"""
def ask_few_shot_cot(q: str) -> str:
response = client.chat.completions.create(
model="gpt-5.4",
messages=[
{"role": "system", "content": f"请按以下格式回答问题:\n{FEW_SHOT_COT}"},
{"role": "user", "content": q},
],
temperature=0.0,
)
return response.choices[0].message.content
print("=== 不使用CoT ===")
print(ask_direct(question))
print("\n=== Zero-shot CoT ===")
print(ask_zero_shot_cot(question))
print("\n=== Few-shot CoT ===")
print(ask_few_shot_cot(question))
|
你可以运行对比三种方式的输出差异。对于鸡兔同笼这类需要列方程的问题,直接回答时模型可能直接抛答案甚至算错,而CoT会先列方程再求解,准确率明显更高。
⚠️ CoT不是万能的。对于简单的事实问答(“法国首都是哪”),加CoT反而拖慢响应、增加token消耗,纯属浪费。CoT的收益在多步推理任务上最大。
4.4.3 自一致性(Self-Consistency)
CoT有一个隐患:同一条推理路径可能因为某个中间步骤错误而导致最终答案错误。比如模型某一步把3+3算成了7,后面就全错。
**自一致性(Self-Consistency)**的思路是:让模型用高温度生成多条不同的推理路径,然后对最终答案做投票,取出现次数最多的作为结果。生活里这就像你拿不准一道题,问了5个同学,5个里有3个说答案是12,那大概率就是12。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# self_consistency.py
# 自一致性提升CoT的稳定性
from collections import Counter
from openai import OpenAI
client = OpenAI()
def solve_with_self_consistency(question: str, n: int = 5) -> str:
"""生成n条推理路径,投票选出最终答案"""
prompt = f"{question}\n\n请一步一步思考,最后用'答案是:X'的格式给出答案。"
answers = []
for _ in range(n):
response = client.chat.completions.create(
model="gpt-5.4",
messages=[{"role": "user", "content": prompt}],
temperature=0.7, # 高温度保证路径多样性
max_tokens=500,
)
text = response.choices[0].message.content
# 简单提取答案:从"答案是:"后面取
if "答案是:" in text:
ans = text.split("答案是:")[-1].strip().split("\n")[0]
answers.append(ans)
# 投票:统计每个答案出现次数,取最多的
if not answers:
return "无法确定答案"
counter = Counter(answers)
best, _ = counter.most_common(1)[0] # 取票数最高的答案
return f"答案:{best}({n}次推理中{counter[best]}次一致)"
# 测试
question = "一个数列:2, 6, 12, 20, 30, ...,请问第10项是多少?"
print(solve_with_self_consistency(question, n=5))
|
自一致性以n倍的推理成本换取更高的准确率,适合对正确性要求高、对延迟不敏感的场景(如离线数据分析)。在线对话场景建议用更小的n(如3),否则用户等不及。
💡 temperature和Self-Consistency的关系:Self-Consistency必须配高temperature(0.7左右)才有意义。如果temperature=0,n次推理结果几乎一模一样,投票就没意义了。
4.5 结构化输出
4.5.1 为什么Agent需要结构化输出
智能体很少是"终点"——它的输出往往要被下游程序消费:解析成JSON塞进数据库、转成函数参数调用其他服务、渲染成UI卡片。如果模型输出的是自由文本,下游就得用正则、字符串匹配去抠数据,脆弱不堪——模型换个说法,正则就匹配不上。
生活里类似:你在餐厅点菜,口头说"我要个番茄炒蛋少放盐多放葱",厨师可能听岔;填一张标准点菜单就稳得多。结构化输出就是让模型"填点菜单"而不是"自由发挥"。
结构化输出(Structured Outputs)让模型直接输出符合预定义schema的JSON,下游程序可以直接反序列化。这是Agent从"聊天机器人"走向"工程系统"的关键一步。
OpenAI SDK提供了两种结构化输出方式:
- JSON Mode:通过
response_format={"type": "json_object"}启用,要求模型输出合法JSON,但不约束schema(字段叫啥、类型是啥都不管)。
- Structured Outputs:通过
response_format={"type": "json_schema", "json_schema": {...}}启用,模型输出必须严格匹配指定的JSON Schema。
💡 JSON Schema是什么? 它是一种描述JSON数据结构的标准格式,类似于"数据模板"。比如"必须有name字段是字符串,必须有price字段是数字"。模型看到这个模板,就会按模板填数据。
4.5.3 用Pydantic定义输出Schema
手写JSON Schema冗长易错,Pydantic能让我们用Python类的方式定义schema,再转成JSON Schema喂给API。下面这个例子让Agent从一段用户描述里提取产品信息,输出成结构化对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
# structured_output.py
# 让Agent输出结构化数据
import json
from openai import OpenAI
from pydantic import BaseModel, Field
client = OpenAI()
# 1. 用Pydantic定义输出结构:就像写一个数据类
class ProductInfo(BaseModel):
name: str = Field(description="产品名称")
brand: str = Field(description="品牌")
price: float = Field(description="当前售价,单位元")
specs: list[str] = Field(description="核心规格列表")
recommendation: str = Field(description="推荐理由,一句话")
# 2. 调用API,启用Structured Outputs
def extract_product_info(user_desc: str) -> ProductInfo:
response = client.beta.chat.completions.parse(
model="gpt-5.4",
messages=[
{
"role": "system",
"content": "你是产品信息提取助手。根据用户描述提取结构化产品信息。",
},
{"role": "user", "content": user_desc},
],
response_format=ProductInfo, # 直接传Pydantic类,SDK自动转JSON Schema
temperature=0.0,
)
# 直接解析成Pydantic对象,不用自己抠字符串
return response.choices[0].message.parsed
# 测试
desc = "我看到一款华为Mate 70 Pro,12GB+512GB版本,售价6999元,麒麟芯片,卫星通话,玄武架构很耐用,推荐买。"
product = extract_product_info(desc)
print(f"产品: {product.name}")
print(f"品牌: {product.brand}")
print(f"价格: {product.price}元")
print(f"规格: {', '.join(product.specs)}")
print(f"推荐理由: {product.recommendation}")
# 也可以直接转成JSON用于下游处理
print("\nJSON输出:")
print(json.dumps(product.model_dump(), ensure_ascii=False, indent=2))
|
client.beta.chat.completions.parse是OpenAI SDK提供的便捷方法,它会把Pydantic类转成JSON Schema、附加到请求中,并把响应解析回Pydantic对象。如果模型输出不符合schema,SDK会抛出异常,便于你做错误处理。
💡 Structured Outputs在GPT-5.4及以上模型上支持完整。配合Pydantic,可以让Agent的输出直接对接FastAPI、数据库ORM等下游系统,几乎零胶水代码。
4.6 ReAct提示模板设计
4.6.1 Thought/Action/Observation格式
ReAct(Reasoning + Acting)是当前Agent最主流的提示范式,由Yao等人在2022年提出。名字是Reasoning和Acting的拼接——一边推理、一边行动。
它的核心是让模型在每一步交替输出思考(Thought)、行动(Action)、观察(Observation):
- Thought:模型推理"我现在该干什么"
- Action:模型选择并调用一个工具
- Observation:工具返回的结果被拼回上下文
- 循环直到模型输出Final Answer
生活里这就是"边想边做"的做事方式:你做一道菜,先想"得先切菜"(Thought),然后去切菜(Action),看看切得怎样(Observation),再想下一步"该热油了"……如此循环直到菜出锅(Final Answer)。
这种范式让模型的行为可解释、可追踪——你能看到它每一步在想什么、调用了什么、得到了什么,而不是一个黑箱直接吐答案。一旦出问题,日志一看就知道是哪步推理跑偏了。
4.6.2 完整的ReAct Prompt模板
下面是一个可以直接使用的ReAct模板,配合一个简单的计算器工具和城市人口查询工具,演示完整的Thought→Action→Observation循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
# react_agent.py
# ReAct范式的最小实现
import json
import re
from openai import OpenAI
client = OpenAI()
# 工具定义:每个工具就是一个普通Python函数
def calculator(expression: str) -> str:
"""安全计算数学表达式"""
try:
# 只允许数字和基本运算符,防注入
if not re.match(r'^[\d\s\+\-\*/\.\(\)]+$', expression):
return "错误:表达式包含非法字符"
return str(eval(expression))
except Exception as e:
return f"错误:{e}"
def search_city_population(city: str) -> str:
"""模拟城市人口查询工具"""
data = {"北京": "2189万", "上海": "2487万", "广州": "1868万", "深圳": "1756万"}
return data.get(city, f"未找到{city}的人口数据")
# 工具注册表:名字 -> (说明, 函数)
AVAILABLE_TOOLS = {
"calculator": ("计算数学表达式,参数:expression(字符串)", calculator),
"search_city_population": ("查询中国主要城市人口,参数:city(字符串)", search_city_population),
}
REACT_PROMPT = """你是一个能使用工具的智能体。请按照以下格式一步步推理和行动:
Question: 用户的问题
Thought: 你思考下一步该做什么
Action: 你要调用的工具名,必须是以下之一:{tool_names}
Action Input: 调用工具的参数,用JSON格式
Observation: 工具返回的结果
...(Thought/Action/Action Input/Observation可重复多次)
Thought: 我现在知道答案了
Final Answer: 最终给用户的回答
可用工具说明:
{tool_descriptions}
开始吧!
""".strip()
def run_react_agent(question: str, max_steps: int = 5) -> str:
"""运行ReAct循环"""
tool_names = ", ".join(AVAILABLE_TOOLS.keys())
tool_descriptions = "\n".join(
f"- {name}: {desc}" for name, (desc, _) in AVAILABLE_TOOLS.items()
)
system_prompt = REACT_PROMPT.format(
tool_names=tool_names,
tool_descriptions=tool_descriptions,
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Question: {question}"},
]
for step in range(max_steps):
response = client.chat.completions.create(
model="gpt-5.4",
messages=messages,
temperature=0.0,
stop=["Observation:"], # 在Observation处停下,等我们去执行工具
)
output = response.choices[0].message.content
messages.append({"role": "assistant", "content": output}) # 把模型输出记回上下文
# 检查是否输出了Final Answer
if "Final Answer:" in output:
return output.split("Final Answer:")[-1].strip()
# 解析Action和Action Input
action_match = re.search(r"Action:\s*(.+?)\n", output)
input_match = re.search(r"Action Input:\s*(.+?)(?:\n|$)", output)
if not action_match or not input_match:
return "智能体无法继续推理"
tool_name = action_match.group(1).strip()
try:
tool_args = json.loads(input_match.group(1).strip()) # 解析JSON参数
except json.JSONDecodeError:
tool_args = {"expression": input_match.group(1).strip()} # 兜底:不是JSON就当字符串
# 执行工具
if tool_name in AVAILABLE_TOOLS:
_, func = AVAILABLE_TOOLS[tool_name]
observation = func(**tool_args) if isinstance(tool_args, dict) else func(tool_args)
else:
observation = f"错误:工具{tool_name}不存在"
# 把Observation拼回上下文,让模型继续推理
messages.append({"role": "user", "content": f"Observation: {observation}"})
return "已达最大步数,未能完成任务"
# 测试:北京和上海人口之和的平方根
question = "北京和上海的人口数字之和的平方根是多少?(人口以万为单位,取数字部分)"
answer = run_react_agent(question)
print("最终答案:", answer)
|
这段代码虽然简化,但完整呈现了ReAct的核心循环:模型输出Thought和Action——程序解析并执行工具——把Observation拼回上下文——模型继续推理。你会在终端看到模型一步步思考:“先查北京人口→再查上海人口→相加→开平方→得出答案”。
⚠️ 生产环境的ReAct实现建议用LangChain或OpenAI的function calling机制,它们对工具调用、错误重试、并发执行有更完善的封装。这里手写是为了让你看清机制本质——会用框架之前,先理解框架在做什么。
4.7 Prompt版本管理与A/B测试
4.7.1 为什么需要版本管理
Prompt是代码,不是一次性文案。一个上线的Agent,其System Prompt会被反复修改:调约束、换示例、改格式。如果没有版本管理,你很快会面临这些问题:
- 改了一处约束,新版本在某些case上变好了,但另一些case悄悄变差了,你却不知道。
- 团队多人协作时,A改的Prompt覆盖了B的版本,回滚困难。
- 线上出问题,说不清是哪个版本的Prompt引入的。
生活里这就像写论文不存稿:改了一版又一版,最后想找回前天那段被删的话,再也找不回来了。所以,Prompt应当和代码一样纳入版本控制:用Git管理Prompt文件、用语义化版本号标注迭代、每次发版前做回归测试。
4.7.2 版本管理最佳实践
推荐的目录结构——按Agent分目录、按版本号分文件:
1
2
3
4
5
6
|
prompts/
├── customer_service/
│ ├── v1.0.0.yaml # 初版
│ ├── v1.1.0.yaml # 增加退换货示例
│ ├── v1.2.0.yaml # 调整语气约束
│ └── latest.yaml # 指向当前线上版本
|
每个Prompt文件用YAML格式,便于标注元信息(版本、模型、温度、变更日志等):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# prompts/customer_service/v1.2.0.yaml
version: "1.2.0"
model: gpt-5.4
temperature: 0.3
description: 客服智能体,v1.2.0调整了语气约束,增加情绪安抚
created_at: 2026-07-05
author: Simon
changelog:
- 增加用户情绪激动时的共情约束
- 关键信息加粗要求更明确
system_prompt: |
你是一名专业的电商客服智能体,名为"小助"。
...
|
💡 语义化版本号怎么读? 格式是主版本.次版本.修订号(如1.2.0)。改约束/换示例这种可能影响行为的改动,升次版本;只改注释/格式不动逻辑的,升修订号;彻底重构走主版本。
4.7.3 简单的Prompt评估框架
光有版本不够,还得能评估哪个版本更好。下面实现一个轻量的Prompt评估框架,用一组测试case对比不同版本的表现:每个case有"输入"和"期望包含的关键词",跑完看通过率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
# prompt_evaluator.py
# Prompt版本评估框架
import json
import yaml
from pathlib import Path
from openai import OpenAI
client = OpenAI()
def load_prompt(yaml_path: str) -> dict:
"""从YAML文件加载Prompt配置"""
with open(yaml_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def call_agent(prompt_config: dict, user_input: str) -> str:
"""用指定Prompt配置调用模型"""
response = client.chat.completions.create(
model=prompt_config.get("model", "gpt-5.4"),
messages=[
{"role": "system", "content": prompt_config["system_prompt"]},
{"role": "user", "content": user_input},
],
temperature=prompt_config.get("temperature", 0.3),
)
return response.choices[0].message.content
# 测试用例:输入 + 期望包含的关键信息
TEST_CASES = [
{
"input": "订单20250704-001还没发货,怎么办",
"must_contain": ["20250704-001", "催促", "发货"], # 答案应包含的关键词
},
{
"input": "收到的手机屏幕有划痕,要退货",
"must_contain": ["退货", "申请"],
},
{
"input": "你好",
"must_contain": ["你好", "请问"], # 问候场景
},
]
def evaluate_prompt(yaml_path: str) -> dict:
"""评估单个Prompt版本在测试集上的表现"""
config = load_prompt(yaml_path)
results = []
pass_count = 0
for case in TEST_CASES:
output = call_agent(config, case["input"])
# 检查是否包含所有期望关键词
hit = [kw for kw in case["must_contain"] if kw in output]
passed = len(hit) == len(case["must_contain"]) # 全部命中才算过
if passed:
pass_count += 1
results.append({
"input": case["input"],
"output": output,
"must_contain": case["must_contain"],
"hit": hit,
"passed": passed,
})
return {
"version": config["version"],
"total": len(TEST_CASES),
"passed": pass_count,
"pass_rate": f"{pass_count/len(TEST_CASES)*100:.1f}%",
"details": results,
}
if __name__ == "__main__":
# 对比两个版本
versions = ["prompts/customer_service/v1.1.0.yaml", "prompts/customer_service/v1.2.0.yaml"]
for v in versions:
if not Path(v).exists():
print(f"⚠️ 文件不存在:{v},请先按4.7.2节创建")
continue
report = evaluate_prompt(v)
print(f"\n版本 {report['version']}:通过率 {report['pass_rate']}({report['passed']}/{report['total']})")
for d in report["details"]:
status = "✅" if d["passed"] else "❌"
print(f" {status} 输入:{d['input'][:20]}... 命中:{d['hit']}")
|
这是一个最小可用的评估框架:用一组带期望关键词的测试case,跑两个Prompt版本,对比通过率。生产中你可以把"关键词匹配"升级成"LLM-as-judge"(用另一个模型给输出打分),评估维度也可以扩展到准确性、完整度、语气等。
💡 什么是LLM-as-judge? 就是用另一个LLM来当"裁判",给被测模型的输出打分。比如让GPT-5.4对客服回复的"礼貌度"打1-5分。比关键词匹配更灵活,但要小心裁判本身的偏见。
💡 评估的核心不是工具多复杂,而是有一组稳定的测试case。建议每次Prompt改动后都跑一遍回归,确保没有"按下葫芦浮起瓢"——这边好了那边坏了。
4.8 小结
本章我们系统学习了智能体开发中的提示工程技术,覆盖了从基础到进阶的完整链路:
- System Prompt是智能体的宪法,包含角色、约束、能力、格式四要素,要用结构化方式书写。
- Few-shot Learning通过示例对齐模型行为,适用于格式控制、风格模仿、小样本分类。
- **思维链(CoT)**让模型先推理后作答,是Agent多步决策的基础;自一致性通过多路径投票进一步提升稳定性。
- 结构化输出让Agent的输出可被程序直接消费,Pydantic + Structured Outputs是当前最佳实践。
- ReAct模板用Thought/Action/Observation循环实现"推理+行动",是当前Agent的主流范式。
- Prompt版本管理与A/B测试让Prompt迭代从"凭感觉改"走向"用数据说话"。
这些技术不是孤立的,而是层层组合的:一个生产级Agent的System Prompt里,往往同时包含角色定义、Few-shot示例、CoT触发语、输出格式约束;它的运行循环则基于ReAct,每一步的输出又被结构化解析。优秀的Prompt工程师不是会背口诀的人,而是能把这套技术按需组合、用数据验证的人。
📌 下一章,我们将深入智能体的工具调用机制——如何让Agent真正"动手"操作外部世界,敬请期待。
4.9 预告:第5章 工具调用机制
智能体之所以叫"智能体"而不只是"聊天机器人",关键在于它能调用工具、执行动作。第5章我们将深入:
- Function Calling的标准协议:OpenAI、Anthropic、Gemini的工具调用接口对比
- 工具注册与发现:如何让Agent动态感知可用工具
- 多工具编排:串行、并行、条件分支的调用策略
- 工具调用失败处理:超时、重试、降级机制
- 自定义工具开发:把任意Python函数包装成Agent可用的工具
Prompt给了智能体灵魂,工具给它双手。下一章,我们给智能体装上双手。