构建一个 AI 智能体——AI智能体的大脑:大型语言模型(LLMs)

148 阅读39分钟

本章内容

  • LLM 的核心能力
  • 如何选择合适的 LLM
  • 使用 LLM 的 API
  • 提示工程(Prompt Engineering)技巧
  • 通过一个 GAIA 基准题动手求解

在全书中,我们将构建一个“研究型智能体(Research Agent, RA)”,用它作为贯穿全书的具体主线来落地相关概念。我们的 RA 需要能够理解诸如“调研 X 的最新工作”“从这些 PDF 中提取关键结论”之类的请求,并做出诸如“信任哪些信息源”“何时应该提出澄清性问题”的决策。

现在把注意力转向智能体的大脑——LLM(见图 2.1 的核心组件)。如图所示,LLM 充当整个智能体系统的推理引擎:它解释用户请求,协调与工具(组件 2)的交互,并驱动智能体的决策过程。与工具一起,LLM 构成了基础智能体(组件 3),我们会在本书前几章中把它搭建出来。

image.png

图 2.1 LLM 作为 AI 智能体的推理引擎。

我们会先讨论如何为智能体选择合适的 LLM——关注的不仅是“性能”本身。全书将使用通过 API 提供的闭源 LLM,并学习如何用这些 API 来实现智能体的大脑。最后我们会深入提示工程——提升 LLM 效果的最关键方法——并以一次上手练习收尾,把学到的知识真正“内化”。

2.1 为智能体选择 LLM

AI 智能体需要一个多才多艺的“大脑”,能够理解多样化请求、处理不同类型的数据并执行复杂任务。大型语言模型(LLM)已经成为理想之选,担当现代 AI 智能体的核心智能。目前,没有其他技术能在综合能力上与 LLM 匹敌,从而让自治智能体成为可能。

图 2.2 展示了 LLM 在 AI 智能体体系中的运作方式。LLM 位于核心位置,处理来自用户的多模态输入、理解其意图,并以文本生成或调用工具等方式灵活响应。通过把输出再次回灌为输入,这种架构支持迭代式问题求解与持续改进。

image.png

图 2.2 LLM 如何作为智能体的中央智能:处理多模态输入、理解意图,并通过文本生成或工具使用来执行多样任务。

接下来是一个务实的问题:我们到底该选哪一个 LLM?想象你要给公司构建客服智能体。你可以选择一个推理能力极强的高性能模型,但价格不菲——一旦要支撑每天成千上万次对话,成本会迅速攀升。你也可以选择一个更省钱的模型,但它是否能充分理解复杂的客户问题?如果你需要为订单查询进行函数调用,但所选模型延迟很高,又该怎么办?

2.1.1 在智能体中使用闭源 LLM

选择 LLM 的第一步,往往是决定用“开源模型”还是“闭源模型”。开源 LLM 可以公开下载权重并自行微调,如 Meta 的 Llama 系列、Mistral 的 Mistral 系列、阿里巴巴的 Qwen 系列、DeepSeek 等。
闭源 LLM 则只能通过 API 访问,如 OpenAI 的 GPT 系列、Google 的 Gemini、Anthropic 的 Claude。本书聚焦闭源 LLM,因为它们为智能体开发提供了更实际高效的路径。

面向智能体开发的即开即用能力
现代智能体开发不仅仅是“文本生成”。闭源提供商通过稳定的 API 提供了强力特性:

  • 结构化输出:以 JSON 等结构化格式返回结果。
  • 函数/工具调用:自然地调用外部工具与 API。
  • 成本优化:内置提示缓存、批处理等以提升 token 利用率。

这些能力在开源模型上也能实现,但通常需要自研或依赖第三方库;闭源模型则可即刻使用、且已在生产环境被验证。

易于使用
构建智能体的核心挑战在于:任务分解、规划、记忆管理、与用户交互的逻辑设计——这些已经足够复杂。若再使用开源 LLM,你还要先准备 GPU 服务、优化模型、管理部署,这会把精力从“智能体逻辑”上分散到“基础设施运维”。闭源 LLM 让我们绕过这些环节,把时间直接用在智能体行为本身。

真实使用中的用户偏好
尽管一些开源模型近期在基准测试上追平甚至超越,但这些分数并不总能代表真实交互场景。在 LMArena 等平台上,用户基于实际交互的偏好排序显示:闭源 LLM 更常胜出。这说明,在智能体关心的维度(理解细微语境、保持上下文、一致地遵循指令)上,闭源模型更可靠。

性能与性价比的均衡
过去,闭源 LLM 高性能、高价格;开源 LLM 更便宜但性能偏弱。过去 2–3 年里,开源模型性能大幅提升,闭源厂商竞争也推动了价格显著下降。如今多数厂商同时提供旗舰与轻量款,且轻量款的性价比很高。
本书之所以采用闭源 LLM,是因为我们构建的是一个基础助手型智能体,强调快速迭代而非定制化建模:闭源 API 自带结构化输出、函数调用、缓存等“工程即战力”,让我们专注于规划、记忆、交互,而非模型托管与性能调优。当然,如果你的场景需要深度微调、特殊分词或安全策略、强合规/本地化要求,或极致成本与时延控制,自建开源模型服务栈会更合适。

2.1.2 闭源 LLM 横向比较

一旦决定选闭源 LLM,下一步就是确定具体的厂商与型号,这会直接影响智能体的能力与运行特性。

类型划分
当下闭源 LLM 的“谁更强”差距在缩小,选择更像是“哪一类模型适合你的需求”。大体分三类,并各有不同档位:

  • 推理型(Reasoning Models) :擅长复杂问题求解,内部采用逐步推理(链式思考),在数学、编程、逻辑、多步分析上表现突出。代价是更长的响应时间与更高的 token 消耗。
  • 非推理型(Non-Reasoning Models) :传统的通用对话模型,优化于通用任务,速度快、效率高,适合实时应用。
  • 混合型(Hybrid Models) :支持可切换思考模式。复杂任务启用“推理”,日常交互回到标准模式,兼顾效果与成本。OpenAI 在 2025 年 8 月发布的 GPT-5 更进一步:可根据用户请求自动决定是否启用推理,相比以往需要手动切换更智能。

表 2.1「模型类型对比」

模型类型档位示例模型优点缺点
仅推理(Reasoning Only)全量(Full)o3-pro, o3;Grok-4• 出色的问题求解能力• 卓越的逻辑推理• 适合复杂分析• 很慢(10–60 秒)• 成本高• 处理简单任务有些“大材小用”
仅推理(Reasoning Only)轻量(Lightweight)o4-mini• 良好的推理能力• 比全量版更快• 更实惠• 仍然比标准模型慢
非推理(Non-Reasoning)均衡(Balanced)GPT-4.1• 出色的通用表现• 响应速度快• 能力多样• 在非常复杂的逻辑上可能吃力
非推理(Non-Reasoning)轻量(Lightweight)GPT-4.1 mini, nano• 极快• 成本极低• 高吞吐• 仅具备基础能力• 不适合复杂任务
混合(Hybrid)自适应(Adaptive)Claude 4 Opus, Sonnet;Gemini 2.5 Pro, Flash• 模式可切换,灵活度高• 可按任务优化• 兼顾两类模型的优势

性能、时延与成本的平衡
在选择 LLM 时需要考虑三个关键因素:

性能(Performance) :模型理解并执行复杂指令的能力如何?这包括逻辑推理、上下文理解以及创造力。一般来说,模型越大,表现往往越好——但并非总是如此。

推理时间/时延(Inference time/Latency) :模型对用户请求的响应速度有多快?对实时交互而言,时延至关重要。关键指标包括首字延迟(TTFT, time-to-first-token)和生成速率(TPS, tokens-per-second)。

成本(Cost) :每个输入/输出 token 的费用是多少?在大规模部署中,成本会决定项目能否可持续。尤其对“代理(agent)”而言,由于涉及检索、工具使用和中间推理,处理的数据量通常比标准聊天机器人更多。

这三个因素常常彼此拉扯:高性能模型通常更慢、也更昂贵;而快速且廉价的模型可能牺牲质量。需要根据代理的目标在三者之间找到平衡。

对于面向客户的对话型代理,用户体验高度依赖响应速度。用户与客服聊天机器人或虚拟助理交互时,即使延迟几秒也会令人沮丧。此类代理既要保持自然的对话流畅度,又要处理多样化问题(从简单 FAQ 到复杂排障)。像 GPT-5 或 Claude 4 Sonnet 这类模型在这里表现出色,因为它们能提供亚秒级响应(TTFT 通常 50–200ms),同时保持良好的理解与生成质量。与旗舰模型相比的轻微性能差距,在日常对话中往往不明显,但速度提升会显著增强用户满意度。

复杂分析型代理的语境完全不同。比如分析合同的法律研究助手,或审阅病史的医疗代理——这里准确性不仅重要,而且至关重要。此类代理常需处理长文档、交叉引用多来源,还要给出带有恰当引用的细致分析。在这种情况下,如果 15–30 秒的等待能换来全面而准确的结果,是可以接受的。OpenAI 的 o 系列、Claude 4 Opus 或 Gemini 2.5 Pro 等高端模型非常适合这类场景,其卓越的推理能力与细节把控足以抵消更高的时延与成本。这些代理通常运行在后台流程或专业工作流中,用户更看重“彻底与可靠”而非“即时”。

高吞吐任务型代理面临规模挑战。无论是分类海量邮件、从文档中抽取数据,还是生成简单报表,当请求量达到数千乃至数百万时,即便每 1000 个 token 便宜 0.01 美元的差距,也会迅速放大成可观的运营费用。像 GPT-5-mini / nano、Claude Haiku 或 Gemini 2.5 Flash 等轻量模型就是为此类场景设计的:在任务定义明确的前提下,它们能保持足够的准确度,同时将成本降低 80%–90%。关键是在确保任务足够“规整”的情况下使用,否则模型容量不足可能引发连锁错误。

由于代理经常把相当一部分推理与决策交给语言模型本身,盲目使用轻量模型容易导致错误累积、结果失真。为避免这种情况,通常应先用高性能模型验证代理的设计与行为;当设计稳定后,再在合适的环节尝试用更小或非推理模型做成本与速度的迭代优化。

为“模型敏捷性”而设计
LLM 市场以惊人的速度演进。新模型几乎按月发布,性能排行榜频繁变动,定价结构也经常更新。今天理想的选择,明天可能就过时。因此,从一开始就为“模型灵活性”而设计至关重要。

要保持信息同步,可以关注这些可靠来源:

  • Artificial Analysisartificialanalysis.ai):提供关于质量、速度与成本的综合对比。]()
  • LMArena Leaderboardlmarena.ai/leaderboard):基于真实交互的用户偏好排名。]()
  • 各提供商的官方公告:获取新模型发布与价格更新信息。

本书的选择:OpenAI API
全书主要使用 OpenAI 的 GPT-5 系列来实现 LLM 智能体。演示与练习中:

  • 基础、简单的示例:用 GPT-5 mini / nano 控制成本;
  • 智能体实现:以 GPT-5 为主。
    其能力充足、可用性高、Python API 友好(含工具调用与结构化输出),能让我们把精力集中在智能体逻辑上。

2.2 为构建 Agent 准备的 LLM API 基础

既然我们已经了解了如何为 AI Agent 选择合适的 LLM,接下来就动手实现构建所需的核心能力。LLM API 的工作方式是:由服务商在其端运行模型,我们发起请求并接收返回结果。我们会讲解如何构造请求以及如何处理返回输出。

要打造一个有效的 Agent,需要掌握三类核心 API 能力:其一,Agent 必须能对用户输入做出自然回应——我们将演示如何发送请求并接收 LLM 的回复;其二,Agent 需要在多轮交互中保持上下文,以便理解长对话或处理多步骤任务;其三,Agent 需要能生成结构化输出,从而不仅能与人或其他 LLM 交互,也能对接现有软件系统。本节聚焦 OpenAI 的 API(已成为事实标准),但这里的模式与实践同样适用于其他厂商。

开始之前,请确保在运行时你的应用可以获取一个有效的 OpenAI API Key。分步配置说明见附录(“API Key 设置”)。本章默认你已在环境中加载了合法的密钥。所有代码示例见 GitHub:github.com/shangrilar/…

2.2.1 响应用户请求:Chat completion

每个 AI Agent 的核心都是:理解用户输入并做出自然、连贯的响应。我们将实现一个最小会话接口:把用户消息发给 OpenAI 的 chat 模型并取回回复。这层会话接口就是用户与 Agent 推理过程之间的主要连接。

先来看一个最小化的初始化流程。三行代码完成三件事:第一行引入 OpenAI 官方 Python 客户端;第二行用 load_dotenv().env 读取环境变量;第三行初始化 OpenAI 客户端(自动从环境变量读取 API Key)。

清单 2.1 载入环境并初始化客户端

from openai import OpenAI 
from dotenv import load_dotenv 
 
load_dotenv()  
                          
client = OpenAI()

在 OpenAI 的产品中,chat 模型用得最广。通过 chat.completions.create 访问。该方法参数很多,但两个最关键:model 指定模型,messages 携带对话内容。

清单 2.2 基本的 chat completion 请求

response = client.chat.completions.create(
    model="gpt-5-mini", 
    messages=[ 
        {"role": "developer", "content": "You are a helpful assistant."}, 
        {"role": "user", "content": [{ "type": "text", "text": "Who's there?" }]} 
    ] 
) 
print(response.choices[0].message.content) 

输出示例:

It's me — ChatGPT, your AI assistant. How can I help? Want to play a knock-knock joke or ask something else?

此例使用 gpt-5-mini:一款轻量、低成本、具多模态能力(可处理文本与图像)的模型。尽管本书主要使用 gpt-5,但这里用 mini 版只是为了演示 chat completion 的工作方式。messages 是消息对象列表,每个对象必须包含 role(如 developer、user、assistant)与 content

我们再用一个推理模型来发同样的请求。把模型改成 o4-mini,并查看 response.usage 中的 Token 使用信息。

清单 2.3 使用推理模型并查看 token 用量

response = client.chat.completions.create(
    model="o4-mini", 
    messages=[ 
        {"role": "developer", "content": "You are a helpful assistant."}, 
        {"role": "user", "content": "Who's there?"} 
    ] 
) 
print(response.choices[0].message.content)
print(f"Input tokens: {response.usage.prompt_tokens}")
print(f"Output tokens: {response.usage.completion_tokens}")
print(f"Reasoning tokens: {response.usage.completion_tokens_details.reasoning_tokens}")

输出示例:

Hello! I'm here to help you. What can I assist you with today?
Input tokens: 23
Output tokens: 127
Reasoning tokens: 113

注意 usage 里出现了 reasoning_tokens。不同于非推理模型(如 gpt-4o),推理模型会在生成可见回答前进行内部的“逐步思考”(chain-of-thought)。这些“思考 token”会被单独统计。即便是简单的问候,上例也用了 113 个推理 token。

注意
如果你刚创建账户并充值了 5 美元,你会立即获得 Tier 1 权限,可使用 o4-mini 等推理模型。仅绑卡(未充值)的免费档无法访问这些模型。

消息有三种 roledeveloper(早期称 system) 用于设置系统提示,定义 AI 的整体行为;user 表示用户消息;assistant 表示模型的回复。开发者可借助 developer/system prompt 设定 LLM 的角色与语气。消息 content 既可直接是字符串,也可用 {type, text} 的显式结构(在传图等多模态场景更实用)。

在实现更复杂的 Agent 时,常见需要调整的不只是基础项。几个常用参数:

  • stream:是否流式接收响应。对“话痨式 UI”、长输出或希望展示阶段性进度时打开;若需“一次成稿再后处理”则关闭。
  • temperature:控制创造性/随机性。0–0.3 适合确定性任务(抽取、评分、代码);0.4–0.7 适合通用写作;0.8–1.0 适合脑暴/文案注意:推理模型不支持 temperature,它们采用固定的结构化思考流程。
  • max_completion_tokens:上限输出 token。长文/逐步推理时调高;想控成本与时延就调低,必要时配 stop 序列。
  • logprobs:返回每个 token 的概率,有助于不确定性估计重排序审计;若要减小返回体积/复杂度可关闭。

更多参数见官方文档:platform.openai.com/docs/api-re…

清单 2.4 流式返回的 chat completion

response = client.chat.completions.create( 
    model="gpt-4o-mini", 
    messages=[ 
        {"role": "system", "content": "You are a helpful assistant."}, 
        {"role": "user", "content": "Who's there?"} 
    ], 
    stream=True, 
    temperature=0.1, 
    max_completion_tokens=200, 
    logprobs=True 
)
for chunk in response: 
    print(chunk.choices[0].delta.content, end="", flush=True) 

输出示例:

Hello! I'm here to help you. What can I assist you with today?None

启用流式后,客户端可在模型尚未完全生成时就开始接收片段,显著改善长回答的体验(降低“主观等待时间”)。流式模式下,响应对象是一个生成器,我们可在 for 循环中逐块处理。

最近,OpenAI 推出了新接口 Responses API,超越基础聊天能力,支持更高级的工具使用(如文件检索、计算机操作)。这反映了对更强 Agent 能力的需求,未来重要性会持续上升。

Responses API 与 Chat Completions 略有不同:不再用 messages,而是用 input 传用户输入、用 instructions 传系统提示;取结果时可直接从 output_text 读取,流程更便利。

清单 2.5 使用 Responses API 的基本请求

response = client.responses.create( 
    model="gpt-5-mini", 
    input="Where is the capital of South Korea?", 
    instructions="You are a helpful assistant." 
) 
print(response.output_text)  

输出示例:

The capital of South Korea is Seoul.

尽管 API 很简洁,但功能强大而灵活。然而,仅“发请求/收回复”还不够打造真正可交互的 Agent。下一节我们将探讨会话管理与上下文保持的关键技巧。

注意
OpenAI 已将 Chat Completions APIAssistants API 统一为新的 Responses API,未来建议优先使用该整合接口。不过,许多 LLM 厂商仍兼容 Chat Completions 的格式,因此本书为兼容性将继续以 Chat Completions 为主进行讲解与实践。

2.2.2 会话管理:短期记忆(short-term memory)

使用 ChatGPT 或 Claude 等对话服务与直接调用 LLM API 的最大区别在于:由谁来管理会话上下文。当我们用 ChatGPT 时,能无缝续聊,是因为服务提供方会在后台自动存储并管理以往对话。而 LLM API 是无状态的(stateless):每次调用都是独立的,不记得之前的交互。这与用户的自然预期相反——人们理所当然地以为 Agent 会“记得”之前说过的话并在此基础上继续。

image.png

(图 2.3 ChatGPT 可以记住用户先前的消息。)

但 LLM API 的工作方式截然不同。就像大多数 Web API 一样,它被设计成无状态。为了让 Agent “记住”对话并在上下文中作答,开发者必须自行管理对话历史,并在每次调用时把它作为输入传给 LLM API。

先用代码确认 LLM API 不会保留先前的会话。下面的示例执行两次独立的 API 调用。

清单 2.6 LLM API 的无状态行为

response_1 = client.chat.completions.create( 
    model='gpt-5-mini', 
    messages=[{"role": "user", "content": "My name is Jungjun"}], 
) 
print(response_1.choices[0].message.content) 

response_2 = client.chat.completions.create( 
    model='gpt-5-mini', 
    messages=[{"role": "user", "content": "What is my name?"}], 
) 
print(response_2.choices[0].message.content) 

输出如下:

Hello, Jungjun! How can I assist you today?
I'm sorry, I don't have information about your name. If you could provide your name, I can respond accordingly.

第一次调用中,模型正确识别并回应了用户的名字。但在第二次调用中,它好像第一次见到你一样作答。这是因为每次 API 调用彼此独立。为了解决这个问题,开发者需要手动管理会话历史。最基础的方式是把所有消息存进一个列表,并在每次请求中附带完整历史

清单 2.7 手动维护会话上下文

messages = [] 
messages.append({"role": "user", "content": "My name is Jungjun"}) 
 
response_3 = client.chat.completions.create( 
    model='gpt-5-mini', 
    messages=messages, 
) 
print(response_3.choices[0].message.content) 

messages.append({"role": "assistant", "content": response_3.choices[0].message.content})
messages.append({"role": "user", "content": "What is my name?"}) 

response_4 = client.chat.completions.create( 
    model='gpt-5-mini', 
    messages=messages, 
) 
print(response_4.choices[0].message.content) 

输出如下:

Nice to meet you, Jungjun! How can I assist you today?
Your name is Jungjun. How can I help you today?

在这个例子里,所有消息按时间顺序存放在 messages 列表中;用户消息标记为 user,AI 回复标记为 assistant,清晰区分了话语方。通过传入完整上下文,我们验证了 AI 能“记住”并正确使用先前提到的名字。

OpenAI 在 2025 年 3 月发布的 Responses API 支持无需手工管理历史的上下文保留。调用 LLM API 时会返回一个响应 ID;把这个 ID 传给下一次调用的参数,服务端就会自动取回上一段对话上下文。

清单 2.8 使用 Responses API 维护会话上下文

response = client.responses.create( 
    model="gpt-5-mini",
    input="My name is Jungjun",
)
print(response.output_text)

second_response = client.responses.create(
    model="gpt-5-mini",
    previous_response_id=response.id, 
    input=[{"role": "user", "content": "What is my name?"}],
)
print(second_response.output_text) 

输出如下:

Nice to meet you, Jungjun! How can I assist you today?
Your name is Jungjun. How can I help you today?

使用 Responses API 时,每个响应都会分配唯一 ID。后续调用只需通过 previous_response_id 指定该 ID,OpenAI 的服务器就会自动维护上下文。

会话管理是 LLM Agent 的核心一环。随着对话变长,token 用量会上升,带来更高成本与更慢响应。在真实部署中,需要策略来管理上下文长度,例如裁剪优先保留重要信息。当我们在第 6 章讲解 Agent 的“记忆”时,会深入这些进阶技巧。

2.2.3 结构化输出:系统集成的桥梁

LLM 生成的自然语言对人类很友好,但对软件系统却充满挑战。LLM 以自由文本形式输出(预测“下一个最可能的词”),人容易读懂,可计算机要求严格的格式——哪怕一个空格或拼写错误都可能让系统崩溃。

当你要把 LLM 接入实际应用(如数据预处理、工作流自动化,尤其是本书关注的 AI Agents)时,这就是重大障碍。因为这些应用都需要把 LLM 的能力与代码驱动的系统衔接起来。因此,LLM 的输出必须结构化可机器解析。从这个意义上说,结构化输出是 Agent 开发中最关键的能力之一。

所谓结构化输出,就是让 LLM 的回复匹配预定义格式。最常见的是 JSON,也可以是 XML、CSV,甚至是特定编程语言的语法。例如,如果我们希望返回一个包含数值键 value 的 JSON,就可以定义相应的 schema

清单 2.9 用于结构化输出的 JSON schema 定义

{
  "type": "object", 
  "properties": {
    "value": { "type": "number" } 
  },
  "required": ["value"]
}

开源 LLM 的 Constrained Decoding(受限解码)
OpenAI 将受限解码能力作为服务的一部分直接提供;而在使用开源模型时,开发者通常需要自己实现,来确保输出格式正确。可以使用如 outlinesgithub.com/dottxt-ai/o…)和XGrammargithub.com/mlc-ai/xgra…)等库,它们与 vLLM、SGLang、TensorRT-LLM 等主流推理框架兼容,详见对应仓库。

OpenAI 的结构化输出用起来非常直观。我们可以用 Python 的 Pydantic 定义目标数据结构,并在 API 调用里通过 response_format 传入。下面的结果清晰展示了模型从输入文本中成功抽取了姓名和邮箱地址。这说明结构化输出不仅能“限定格式”,还能作为智能抽取/转换的工具。

清单 2.10 使用 Pydantic 模型解析结构化输出

class User(BaseModel):
    name: str
    email: str
 
response = client.beta.chat.completions.parse(
    model='gpt-5-mini',
    messages=[{"role": "user", "content": "My name is John Smith, 
➥my phone number is (555) 123-4567, 
➥and my email is john.smith@example.com"}],
    response_format=User,
)
 
print(type(response.choices[0].message.parsed))
print(response.choices[0].message.parsed)

输出如下:

<class '__main__.User'>
name='John Smith' email='john.smith@example.com'

结构化输出的应用几乎无限:可以把自然语言转成数据库查询,自动生成复杂配置文件,或抽取API 调用参数。在 Agent 开发中,它对把用户意图转化为结构化行动、或为工具调用生成参数至关重要。

总之,结构化输出是 LLM 的“语言生成”与软件系统“严格规则”之间的桥梁。它让我们能以可控、可靠的方式释放 LLM 的能力,是 AI Agent 开发的基础能力之一。

2.2.4 异步 API 调用:一次处理多条请求

在构建 AI Agent 时,常常需要同时向 LLM API 发送多条请求。比如:想把同一条提示词在多个模型上测试、并行处理一批用户查询、或进行多次评测以做基准测试。

如果把这些请求依次(同步)发送,总耗时会变成各次请求耗时之和。由于每次 API 调用都包含网络时延和模型计算时间,总耗时会迅速增加。

异步执行允许我们几乎同时发起多条请求,并在每个请求完成时就地处理其结果,而不必等待更早的请求结束。在 Python 中,这可通过 async/await 语法与内置的 asyncio 库实现。

其核心思想是:异步函数(协程) 可以在特定位置暂停(例如等待网络响应时),把执行权让给其他任务。这样程序无需被某个慢请求“卡住”,能在等待期间继续干别的事。

OpenAI 的 Python 库通过 AsyncOpenAI 客户端支持这种用法。它的用法与常规 OpenAI 客户端几乎一致,但能发起非阻塞请求。下面的示例展示了如何并行向 LLM API 发送三条提示,并在全部完成后打印结果。

清单 2.11 异步调用 LLM

import asyncio
from openai import AsyncOpenAI
 
client = AsyncOpenAI()
 
async def get_answer(prompt):
    response = await client.chat.completions.create(
        model="gpt-5-mini",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content
 
async def main():
    prompts = [
        "Hello!",
        "What's 2 + 2?",
        "Tell me a short joke about cats."
    ]
    
    tasks = [get_answer(p) for p in prompts]
    
    results = await asyncio.gather(*tasks)
    
    for r in results:  
        print(r)
 
asyncio.run(main())

在这个例子中,get_answer()async def 定义,并通过 await 等待 API 调用完成。当某个请求在等待响应时,其他请求可以并行进行。main() 函数创建任务列表并交给 asyncio.gather(),后者会并发运行所有任务,并一次性返回全部结果。

当需要处理大批量 API 调用时,这种方式尤为有价值。贯穿全书,你会在 GAIA 基准测试、多 Agent 应用,以及其他强调效率与吞吐的场景中看到异步执行的用法。一旦理解了这个基础模式,你就可以在本书更复杂的 Agent 工作流中反复复用它。

2.3 提升 Agent 智能:提示词工程(Prompt Engineering)

当我们选定 LLM 作为 Agent 的“大脑”后,下一步就是让这颗“大脑”发挥最佳水平。提示词工程在这里至关重要——它是用来精心编写指令、让 LLM 持续、可靠地产生目标输出的一门“艺术与科学”。

2.3.1 面向 Agent 的提示词工程

提示词工程是最快、最直接控制 Agent 行为(人格、角色、决策方式——包括何时使用工具)的方法。Agent 依赖两类提示词运行:系统提示用户提示

  • 用户提示(User prompts) :用户在聊天里输入的内容。它每一轮都会变化,开发者无法控制。
  • 系统提示(System prompts) :由开发者定义、在会话中持续生效的指令。它设定语气、权限、约束与工具使用策略。

本书聚焦系统提示设计。一个设计良好的系统提示能把通用 LLM 变成任务可靠的 Agent。尽管我们无法控制用户提示,但可以借助系统提示让 Agent 在面对各种用户输入时保持一致性。可参考 Anthropic 公布的 Claude 系统提示,这是一个很好的例子(docs.anthropic.com/en/release-…)。

<Instruction>

The assistant is Claude, created by Anthropic.

(…)

“Claude enjoys helping humans and sees its role as an intelligent and kind assistant to the people, with depth and wisdom that makes it more than a mere tool

(...)

"Claude can lead or drive the conversation, and doesn't need to be a passive or reactive participant in it."

(…)

If Claude is asked about a very obscure person, object, or topic, (...) it warns that it may hallucinate and recommends that the person double-check its information (...)

提示词设计的两个目标

在 Agent 开发中,提示词工程需要同时实现两个主要目标:

  1. 提升解题能力
  • 引导模型准确地完成复杂任务
  • 鼓励逻辑推理逐步思考
  • 减少错误、提升精度
  1. 保持一致的人设与语气
  • 定义 Agent 的身份/人设
  • 让沟通风格匹配品牌
  • 在不同语境中提供合适的情绪回应

这两个目标是互补的。高精度但语气不当会损害体验;友好但无效的回答同样没有价值。

更好解题的提示策略

角色设定(Assigning Roles)

让 LLM 扮演特定角色/人设,能显著提升其专业度,使其用词、语气与视角更贴近该角色(参见 Anthropic 与 OpenAI 文档)。例如告知模型“你是一位资深法律专家”,它就更可能使用法律术语、引用相关案例,而不是泛泛而谈。定义人设时,最好不仅给职称,还要包含背景、资历与专长领域。角色设定通常通过系统提示完成,尤其适用于回答复杂、领域特定的问题。

你是一名具有 10 年自然语言处理与机器学习模型优化经验的资深数据科学家。

清晰直接地写(Write clearly and directly)

提示词应详尽且明确,就像对从未谋面的人下达指令/提出请求。含糊或拐弯抹角易导致误解。对比如下:

  • 含糊的指令: “帮助用户处理日程安排需求。”
  • 清晰的指令: “当用户请求日程安排协助时,先识别会议类型(1v1、团队会、外呼),再收集必要信息(参与者、时长、可选时间、限制条件),最后给出三个具体可选时间段,并为每个选项给出说明。”

给 LLM 下指令时,最好包含相关上下文(目的与目标) ,同时剔除无关信息。对复杂请求,拆成步骤,并指定期望格式/篇幅

使用结构化表达(Using structured expression methods)

结构化格式有助于模型理解与处理信息。与“清晰直接”理念相通,但更强调模型在训练中见过的格式。

Markdown / XML / JSON / HTML 等格式在训练语料中很常见,模型对其更熟悉。结构化在复杂提示中尤其有用,能把指令示例等组成部分清晰分隔,便于模型解析。

<Instruction>
Please provide a solution for the following situation.
</Instruction>

<Situation>
There is a communication breakdown among team members.
</Situation>

<Solution>
[AI-generated solution goes here]
</Solution>

少样例提示(Few-shot prompting)

Few-shot是在提示中提供示例,让 LLM 明白任务类型与期望输出样式。无需再训练,就能显著提升模型在特定任务上的表现。这也称为上下文学习(In-Context Learning) 。它对提升准确性格式一致性语气统一非常有效。但其效果高度依赖示例质量。优质示例应满足:

  • 相关性:示例要与目标任务高度匹配
  • 多样性:覆盖多种输入;若过于相似,模型可能“套模板”
  • 格式一致:所有示例遵循同一结构;可用 <example> 等标签明确分隔

示例:让模型识别邮件的类别紧急程度

<example>
Input: The project deadline has moved up. Please send me the updated draft by EOD today.
Category: Work
Urgency: High
</example>

<example>
Input: Our biggest sale of the year is on now—check out 40% off all items!
Category: Marketing
Urgency: Low
</example>

<example>
Input: Get rich quick with this crypto opportunity. Click now!
Category: Spam
Urgency: Low
</example>

思维链(Chain of Thought,CoT)

解决复杂问题时,人类会把问题拆解成小步并逐步推理。思维链提示引导 LLM 显式写出中间推理步骤,因此特别适合复杂问题求解。与其直接要最终答案,不如要求模型先描述推理过程。图 2.4展示了 CoT 的威力:通过逐步推导,模型从错误答案(27)转向正确答案(9)。

image.png

CoT 优势:

  • 更强的推理力:把任务拆解,让模型逐步解决,降低出错率;尤其适合数学、逻辑与分析
  • 可解释性:模型展现“如何得到结论”,便于审计逻辑、定位链路错误

注意:CoT 会增加输出长度、拉长响应时间。如果任务不需要深度推理,为了效率可不使用 CoT

2.4 用 LLM 解题

到目前为止,我们已经讨论了使用 LLM API 与提示工程的理论。但只有理论还不够。让我们把所学应用到一个真实问题上,用 LLM API 实操,并测试多种提示工程技巧。

我们将做一个对照实验,观察:

  • 模型大小如何影响表现(gpt-5 vs gpt-5-mini)
  • 模型类型如何影响推理(非推理模型 vs 推理模型)
  • 提示工程技巧如何影响准确率

这种动手方式会同时揭示当下 LLM 在实践中的威力局限。从一个实际场景开始:假设我们在做一个帮助用户为活动做筹备的购物助手。该助手必须能根据复杂的家庭描述准确计算数量——多买或少买都会让客户不满意。下面是助手可能收到的一条真实请求:

我的家庭聚会在本周,我被分配带土豆泥。参加者包括我已婚的母亲和父亲、我的双胞胎兄弟及其家庭、我的姑/姨妈及其家庭、我的外婆/奶奶和她的兄弟、她兄弟的女儿,以及那位女儿的家庭。除了我之外,所有成年人都曾结婚,没有人离婚或再婚,但我的外公/爷爷和我外婆/奶奶的嫂/弟媳去年去世。所有健在的配偶都会参加。我的兄弟有两个仍是孩子的孩子,我的姑/姨妈有一个六岁的孩子,而我外婆/奶奶的兄弟的女儿有三个不满 12 岁的孩子。我估计每位成年人吃大约 1.5 个土豆,每个孩子吃大约 1/2 个土豆,但我的表侄/堂外亲(second cousins)不吃碳水。平均每个土豆约半磅,土豆按 5 磅一袋出售。我需要买几整袋土豆?只给数字。

“土豆题”

顾客需要决定家庭聚会要买多少土豆。难点包括:

  • 解析复杂的家庭结构描述
  • 识别谁会实际参加
  • 区分**成年人(1.5 个/人)孩子(0.5 个/人)**的份量
  • 考虑饮食限制(部分家庭成员不吃碳水)
  • 把总量换算成 5 磅装的袋数

这类多步推理问题非常能代表 LLM 在真实应用中面临的挑战:需要仔细的阅读理解逻辑推断基础算术——而这些方面提示工程都能起到显著作用。

为什么选这个问题

我们选取的不是随便的例子,而是来自 GAIA(General AI Assistants) 基准的题目。GAIA 由 Meta、Hugging Face 与 AutoGPT 团队在 2023 年合作发布,包含 466 个精心设计的问题,重点考察实际解题能力而非简单的事实回忆。更多 GAIA 细节会在第 10 章介绍,这里只给必要背景,直接进入练习。

GAIA 将问题分成三个难度等级:

  • Level 1:相对简单,不用工具或只用一个工具
  • Level 2:中等复杂,需要组合 5–10 个工具
  • Level 3:最复杂,需要多工具与多步骤

本练习使用 Level 1 的题目,比较不同提示工程方法的效果。之所以选它,是因为它很贴近日常通用助理会遇到的推理任务。看似简单,实则要求模型:

  • 解析含有隐含信息的自然语言
  • 搭建家族关系的心智模型
  • 同时应用多条约束
  • 基于抽取的信息进行计算

通过用不同提示工程方法解这题,我们可以观察这些方法如何影响模型处理现实复杂度的能力,从而看到当前 LLM 的优势与限制。

题目分析与期望答案

按步骤解题:先数成年人,再数儿童。共有 11 位成年人6 个孩子中有 3 个不吃碳水。按成年人每人 1.5 个、儿童每人 0.5 个计算,总计需要 18 个土豆。每袋 5 磅,一袋约 10 个土豆,因此需要 2 袋

步骤 1:列出全部参加者(成人)

  • 我(未婚成人)
  • 父母:母亲、父亲(2 位成人)
  • 双胞胎兄弟 + 配偶(2 位成人)
  • 姑/姨妈 + 配偶(2 位成人)
  • 外婆/奶奶(1 位成人,丧偶)
  • 外婆/奶奶的兄弟(1 位成人,丧偶)
  • 外婆/奶奶的兄弟的女儿 + 配偶(2 位成人)

步骤 2:统计孩子

  • 兄弟的孩子:2 个
  • 姑/姨妈的孩子:1 个
  • 外婆/奶奶的兄弟的女儿的孩子:3 个(这些是我的 second cousins)

步骤 3:应用食量规则

  • 成人:11 × 1.5 = 16.5
  • 普通孩子:3(兄弟 2 + 姑/姨 1)× 0.5 = 1.5
  • second cousins:3 × 0(不吃碳水)= 0
  • 合计:16.5 + 1.5 = 18 个土豆9 磅2 袋

为了测试而做的改动

在开发这项练习时,我们发现原题问“需要几袋 5 磅装土豆”,即使中间计算少算了人,模型也常常仍答“2 袋”。为更精确评估,我们将问题修改为直接问需要多少个土豆

平均每个土豆约半磅,按 5 磅一袋出售。我需要买几整袋土豆?只给数字。
我总共需要多少个土豆?只给数字。

接下来我们准备系统提示,以便使用 2.2.3 节介绍的结构化输出。定义一个 PotatoSolution 类,包含模型的思考过程 thought_process 与最终答案 final_answer。系统提示的其余部分因篇幅省略。

# 列表 2.12 为 GAIA 任务定义响应格式与系统提示
class PotatoSolution(BaseModel):
    thought_process: str
    final_answer: str
    
SYS_PROMPT = """You are a general AI assistant. 
I will ask you a question. Report your thoughts in "thought_process" and finish your answer in "final_answer".
(rest omitted)
"""

为了获取 LLM 的响应,我们定义一个函数,传入 client、模型名、提示与响应格式。本练习比较四个模型:gpt-4.1、gpt-4.1-mini、gpt-5、gpt-5-mini。其中 gpt-4.1 与 gpt-4.1-mini 为不同大小的非推理模型,gpt-5 与 gpt-5-mini 为带内置 CoT 能力的推理模型。对非推理模型我们设定 temperature=0.5 以引入一定输出差异(因为每组要跑 10 次看一致性);推理模型不支持温度。系统提示用 GAIA 的系统提示,随后在各测试中变更用户提示。

# 列表 2.13 获取 GAIA 任务解的函数
def get_llm_solution(
    client: OpenAI,
    model_name: str,
    prompt: str,
    result_format: type[BaseModel] | None = None
) -> str:
    try:
        api_call_params = {
            "model": model_name,
            "messages": [
                {"role": "system", "content": SYS_PROMPT},
                {"role": "user", "content": prompt}
            ]
        }
        if model_name not in ["gpt-5", "gpt-5-mini"]:
            api_call_params["temperature"] = 0.5
        
        response = client.beta.chat.completions.parse(
            **api_call_params,
            response_format=result_format
        )
        
        parsed_object = response.choices[0].message.parsed
        return parsed_object.final_answer
        
    except Exception as e:
        print(f"Error during LLM API call for model {model_name}")
        return ""

我们测试了五种提示策略:

  1. 基线(Zero-shot) :不加额外上下文,直接把原问题传给模型。
# baseline (Zero-shot prompt)
baseline_prompt_english = f"""
{FAMILY_REUNION_PROBLEM}
"""

2. 少样例(Few-shot) :加入一个相似的用餐计算示例。给出一个生日会的算例,帮助模型学习如何解析家庭关系与应用食量规则。

few_shot_prompt_english = f"""
Here's an example of how to solve a similar family calculation problem:

<example>

Question: "I'm hosting a birthday party. Attendees include me, my parents, my sister and her husband, and my uncle with his two teenage children. Each adult will eat 2 slices of pizza and each child will eat 1 slice. How many pizza slices do I need?"

Answer: 14

</example>

Now solve this problem:

{FAMILY_REUNION_PROBLEM} 
"""

3. 角色设定(Role prompt) :引入人设 “你是一位家庭活动筹划专家”,看这种框架是否提升任务求解效果。

role_setting_prompt = f"""
You are a family event planning specialist with expertise in calculating food quantities for family gatherings. You excel at parsing complex family relationships and determining accurate serving quantities based on different demographics and dietary preferences.

Using your expertise, please solve this problem:

{FAMILY_REUNION_PROBLEM} """

4. 引导式 CoT(Guided CoT) :把任务拆成步骤:识别参加者、检查特殊条件、进行计算。

guided_cot_prompt = f"""
{FAMILY_REUNION_PROBLEM} 
Let's solve this step by step:

First, identify all family members attending:
List each person and their relationship to you
Account for spouses of married individuals
Note any deceased family members who won't be attending
Categorize attendees by age group:
Count total adults
Count total children
Note any special dietary restrictions
Apply consumption rules:
Calculate potatoes needed for adults
Calculate potatoes needed for children
Adjust for any dietary restrictions
Sum the total number of potatoes needed Please work through each step carefully. """

5. 简洁 CoT(Simple CoT) :只添加一句 “Let’s think step by step.”

simple_cot_prompt = f"""
{FAMILY_REUNION_PROBLEM} 
Let's think step by step. """

由于 LLM 具有非确定性,每次运行结果可能不同。为得到更可靠评估,我们对每个“提示 × 模型”组合运行 10 次,统计正确答案的比例。结果如下:

表 2.2 “土豆题”实验结果(正确率)

Baseline (Zero-shot)Few-shotRole promptGuided CoTSimple CoT
gpt-4.10.90.91.00.90.7
gpt-4.1-mini0.00.20.10.30.0
gpt-51.01.01.01.01.0
gpt-5-mini1.00.80.61.01.0

注:表中数值表示 10 次运行中的正确率比例(例如 0.9 代表 90%)。

2.4.1 实验看 LLM 表现:提示工程的威力与边界——以及为何必须用工具

从实验结果中出现了几个有趣的模式。首先,少样例(Few-shot)提示的强大效果十分明显。GPT-4.1 在基线设置下的准确率只有 20%,但加入 Few-shot 提示后提升到 100%。这表明具体示例能显著增强模型理解

其次,推理模型与非推理模型的差异非常显著。推理模型 o3 即使在基线提示下也能达到 100% 的准确率,而标准模型需要提示工程的辅助才能表现良好。更有意思的是,对推理模型而言,引导式 CoT 提示反而降低了表现。这暗示推理模型内部已经在进行逐步思考,外部强加的结构可能干扰其原生推理过程

第三,角色提示(Role Prompt)的局限性也很明显。在很多情况下,加入角色设定并没有提升表现——有时甚至会变差。这说明仅仅赋予“专业人设”并不必然提升问题求解能力

需要再次强调的是,LLM 每次生成的输出都可能不同,因此上述结果并不一定在每次运行中完全复现。单次实验不足以对性能下定论

通过这次动手练习,我们确认提示工程可以大幅提升 LLM 表现,也直观体验了推理模型的威力。然而,LLM 仍有根本性局限。看看下面这个问题:

如果 Eliud Kipchoge 能无限地保持其创造世界纪录的马拉松配速,
在地月距离的近地点时,他跑完这段距离需要多少千小时
计算时请使用维基百科月球页面上的最小近地点值。
将结果四舍五入到最接近的 1000 小时,必要时不要使用逗号分隔符

要解这道题,你需要在维基百科上找到地月最短距离,查到 Kipchoge 的马拉松纪录配速,然后进行准确的数学计算。在这里,LLM 的两个基本限制会暴露出来:

  1. 知识限制。LLM 只“知道”训练数据里包含的信息,而且即便包含,也不能保证数值准确。像 Kipchoge 的纪录或地月距离这类事实或许在训练数据中,但在海量语料里只占极小一部分,准确可用并无保证
  2. 计算限制。LLM 通过预测下一个词来生成文本,对精确数学运算比较脆弱。尤其当涉及多步复杂计算时,错误很容易逐步累积

要克服这些限制,LLM 必须能够使用外部工具:通过联网搜索获取最新信息,使用计算器进行精确计算,并组合多种专用工具来解决复杂问题。这正是我们下一章将要讨论的工具使用(Tool Use)的核心——我们会详细讲解如何为 LLM 提供工具,以及 LLM 如何选择并正确使用这些工具。现在,让我们去用一些工具吧。

2.5 小结

  • LLM 之所以成长为强大的智能体“大脑”,离不开三项关键进展:通过自监督学习处理非结构化数据(文本、图像、音频)、通过指令微调(SFT、RLHF)理解用户意图,以及随着 o1、DeepSeek-R1 等模型引入的推理能力来处理通用任务。
  • 闭源 LLM在智能体开发上具备实用优势:开箱即用的函数调用与结构化输出、稳定 API 让开发者聚焦于智能体逻辑而非基础设施,以及在真实交互中的更高用户偏好
  • 多模型架构提供最大灵活性:可按任务难度优化成本(简单任务用便宜模型)、发挥各模型的优势、通过自动故障切换提高可靠性,并为新模型提供安全试验场
  • 提示工程能显著提升 LLM 表现:包括角色赋予(给定具体人设)、清晰结构化表达(XML/Markdown)、**少样例(Few-shot)示范、以及链式思考(CoT)**把复杂问题拆解为步骤。
  • LLM 存在知识边界计算约束等根本限制,因此需要借助外部工具——这也是下一章的重点。