Contents

提示工程进阶:为智能体设计高效Prompt

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从"聊天机器人"走向"工程系统"的关键一步。

4.5.2 OpenAI的response_format参数

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的核心循环:模型输出ThoughtAction——程序解析并执行工具——把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给了智能体灵魂,工具给它双手。下一章,我们给智能体装上双手。