Python RAG 实战手册——Agentic RAG

0 阅读41分钟

Agents 是一种系统,在其中 LLM 充当决策者。模型会观察当前情况,规划一系列 actions,并选择 tools 来执行这些 actions。就像人类会在火车取消时调整旅行计划一样,agent 会随着新信息出现而调整策略,见图 8-1。

image.png

图 8-1:使用 agents 解决复杂问题,模拟人类行为

这种看待问题解决的方式并不是一夜之间出现的。早期 LLM 应用被构建为孤立功能,例如 summarization 或 translation。随着时间推移,这些功能发展成 multistep workflows,最终发展为完全 agentic systems,可以自行决定使用哪些 tools。

图 8-2 展示了从单一 LLM 功能,到 orchestrated workflows,再到能够自由选择和组合 tools 的 autonomous agents 的演进过程。

image.png

图 8-2:从单一 LLM 功能到自主 agentic systems

一旦你把 agent 看成一个自主的问题解决者,它的内部结构就更容易理解。核心上,agent 运行在一个连续循环中。它执行一个 action,观察结果,然后决定下一步做什么。

图 8-3 展示了使这一过程成为可能的主要组件。Agent 使用 LLM 进行 reasoning 和 planning,依赖 tools 执行 actions,并将信息存储在 short-term 和 long-term memory 中,使过去结果能够影响未来决策。

image.png

图 8-3:典型 agent 架构由用于决策的 LLM、用于执行动作的 tools,以及用于上下文的 memory 组成

实现这个循环没有唯一方式。存在多种 planning patterns,但最广泛使用的是 ReAct。在该模式中,agent 会在 reasoning about what to do next 和 acting by calling tools 之间交替,并使用结果来改进计划。循环会持续,直到 agent 达到目标,或触及预定义限制,例如最大 tool calls 数量。ReAct 由 Shunyu Yao 等人在 2023 年论文 “ReAct: Synergizing Reasoning and Acting in Language Models” 中提出。ReAct 将显式推理与 tool use 结合起来,使 agent 能够调整自己的问题解决策略,而不是遵循固定 workflow。

为了看看这在实践中如何展开,图 8-4 展示了一个 agent 处理具体问题的过程。一个复杂问题被拆解为更小、可搜索的 subquestions,信息从 web 中被检索出来,然后结果被组合以生成最终答案。

image.png

图 8-4:Agentic system 将复杂问题拆分成可管理步骤,收集信息,并执行计算以得出结论

本章将在这些思想基础上,介绍用于实现 agentic RAG systems 的 tools 和 frameworks,从低层 function calling,到能够管理 state、memory 和 tool orchestration 的高层 runtimes。

你将看到 recipes,用 direct function calling 或 OpenAI Agents SDK、LangGraph 等 orchestration frameworks 构建 agents。它们分别代表从轻量 orchestration 到完整 agent runtimes 的不同抽象层级。除此之外,LlamaIndex、CrewAI 和 AutoGen 等 frameworks,在比较构建 agentic systems 的不同方法时也值得探索。

在探索这些 frameworks 之前,你需要理解它们编排的 building blocks。每个 agentic system 都依赖 tools,也就是执行特定 actions 的离散 functions。第一个 recipe 展示如何从零设计一个 custom tool。

你可以在本书 GitHub repository 中找到本章所有代码示例。

8.1 在 Python 中设计 Custom Tool

Problem

你需要为自己的 agent toolkit 定义一个 custom tool。

Solution

创建一个带有详细 docstring 的 Python function,用来描述它的目的、输入和输出。Docstring 使 LLM 能够理解什么时候以及如何使用这个 tool:

import requests

def get_weather(latitude, longitude):
    '''
    This function calls the open-meteo API to get the weather data for a
    given latitude and longitude.

    Args:
        latitude (float): The latitude of the location to get weather
            data for.
        longitude (float): The longitude of the location to get
            weather data for.

    Returns:
        dict: A dictionary containing the weather data for the given
            latitude and longitude.
    '''
    response = requests.get(
        "https://api.open-meteo.com/v1/forecast"
        f"?latitude={latitude}"
        f"&longitude={longitude}"
        "&current=temperature_2m,wind_speed_10m"
        "&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m"
    )
    data = response.json()
    return data['current']['temperature_2m']

在 agentic system 中,LLM 会根据用户请求决定什么时候调用这个 tool。例如,如果用户问:“What is the weather like in New York tomorrow?”,LLM 会识别出这个 tool 可以提供答案,填入参数,并调用函数。函数会检索天气数据,并将结果返回给 LLM,供后续处理。

Discussion

Tools 是通过能够执行某个 action 的代码实现的。在 Python agents 中,tools 可以是 functions、API calls、database queries 或 mathematical operations。

设计良好的 tools 会通过 docstrings 定义其 purpose、inputs 和 outputs。LLM 会读取这些 docstrings,以判断哪个 tool 适合当前任务以及如何调用它。这使 agents 能够在 multistep workflows 中串联 tools,而无需 hardcoded logic。

Custom tools 适合 domain-specific functionality,例如内部数据库、专有 APIs 或业务计算。每个 tool 应保持单一职责。避免为已有 prebuilt solutions 能处理的任务创建 tools,例如 web search 或 code execution。

注意,每个 tool 都需要随着 APIs 变化而进行 testing、debugging 和 updates。调用外部服务的 tools 需要处理 timeouts、rate limits 和 failures。

See Also

OpenAI Agents SDK tool creation guide 提供了如何将 Python functions 转换为 agent tools 的教程。

OpenAI Function Calling guide 包含关于为 LLM function calling 设计和实现 tools 的官方文档。

Anthropic tool use documentation 提供了使用 Claude models 定义和实现 tools 的综合模式。

8.2 在 Multiagent Systems 中使用 Workflow Patterns

Problem

你希望通过为 tools 和 sub-agents 使用合适的 workflow patterns,优化 agentic system 的性能和延迟。

Solution

这个 recipe 描述 Anthropic 定义的五种常见 workflow patterns。你可以将它作为参考,选择适合 use case 的模式。具体实现请参考本章后续 recipes,尤其是 Recipe 8.5 的 parallel tasks 和 Recipe 8.8 的 orchestrator-workers patterns。

这些模式之间的关键区别在于,有些高度依赖 concurrency,也就是任务并行运行;另一些则关注 sequential steps,也就是每一步都基于前一步输出。你需要哪种方法取决于具体 use case:有时你需要 sequential workflow,有时你可以让部分任务并行运行。

Workflow pattern 1:Prompt chaining

在 prompt chaining 中,workflow 将复杂任务拆分为连续步骤,每次 LLM call 都处理前一步的输出。

图 8-5 展示了一个从 PDF 中提取文本并将其转换为简单英语的 workflow。该 workflow 将 subtasks,例如 extraction、translation、reformulation,拆成独立 LLM calls。由于每次 call 都依赖前一次输出,因此这个 workflow 必须是 sequential 的。

image.png

图 8-5:Prompt chaining workflow 按顺序连接 prompts,以构建上下文

Workflow pattern 2:Routing

在 routing workflow 中,初始 LLM call 充当 router,分析请求并选择最合适的 tool 或 subworkflow。

图 8-6 展示了一个接收 email 并生成 response 的 router。如果 email 包含 attachments,router 会触发 processing steps 来分析、预处理和保存内容。对于简单 email,router 会直接使用来自 calendar 或 vector store 的信息作答。大约 5 到 10 个 scenarios,就可以覆盖处理 email 所需的大多数 actions。

image.png

图 8-6:在 routing workflow 中,LLM 根据输入决定走哪条路径

Workflow pattern 3:Parallel tasks

在 parallel tasks workflow 中,你会并行运行多个 LLM calls 或 tool calls,每个 call 独立处理任务的不同部分。最后,aggregator 会收集并合并结果。

图 8-7 展示了一个 agent 分析长扫描合同的过程。系统必须逐页提取文本并分析。为了加速处理,可以每次将一页发送给 multimodal model,以提取 expiration date 和 cancellation terms 等特定值。

即使使用 efficient model,顺序处理每页也需要 10 到 20 秒。并行运行 10 或 20 个 LLM calls,可以将处理时间从数小时降低到数分钟。

主要约束是 API endpoint 的 token-per-minute limit。并发运行过多 tasks 可能触发 token limit 并导致错误。

image.png

图 8-7:在 parallel task workflow 中,多个并发 LLM calls 会并行分析合同的多个页面

TIP

Recipe 8.5 使用完全相同的概念并行读取和分析文档。每次 LLM call 读取并分析一页,aggregator 合并结果。

Workflow pattern 4:Orchestrator-workers

Orchestrator-workers workflow 使用一个 agent 充当 coordinator。Orchestrator 可以访问多个 worker agents,每个 worker agent 专门负责特定任务或领域。

图 8-8 展示了一个 orchestrator,它可以访问三个 worker agents:

  • 一个 worker 将用户请求转换为合适 SQL query 并执行,从而查询 SQL database。
  • 一个 worker 搜索 wiki data,例如通过 vector database 和 semantic search。
  • 一个 worker 使用 specialized prompt 执行直接 LLM call。

根据用户问题,orchestrator 可以调用一个或多个 workers。随后,synthesizer 会考虑所有收集到的内容,并为用户生成一个单一最终 response。

image.png

图 8-8:在 orchestrator workflow pattern 中,orchestrator 决定调用哪些 specialized workers

Workflow pattern 5:Evaluator-optimizer

在 evaluator-optimizer pattern 中,一个 LLM 创建初始草稿,另一个 LLM 进行审查。系统会在 drafting 和 evaluation 之间迭代,直到 evaluator 满意,或达到最大迭代次数。

图 8-9 展示了一个用于撰写技术博客文章的 agent system。一个 LLM 生成 draft paragraphs,evaluator 提供反馈。该循环持续,直到 evaluator 批准结果。

image.png

图 8-9:在 evaluator-optimizer workflow 中,输出通过 evaluation 和 optimization cycles 被迭代精炼

Discussion

Sequential patterns 适用于任务按顺序发生的场景,每一步处理前一步输出。当数据线性流动时,例如先分析文档再总结文档,可以使用这种方法。

Parallel patterns 会并发运行独立任务。当 subtasks 彼此不依赖时,例如同时查询多个 databases,可以使用这种技术。

Routing patterns 会对输入分类,并相应选择 execution paths。当你有不同 task types 时,例如将客户 query 路由到 sales、support 或 billing,可以使用 routing patterns。

Orchestrator patterns 会协调 specialized sub-agents。当任务需要多样专业能力时,可以使用这种策略。一个 orchestrator 会委派给 domain-specific agents。

当可靠性比灵活性更重要时,选择 workflow patterns。相反,当任务需要 adaptive decision making 时,可以选择 autonomous agents。在第二种情况下,agent 会在运行时决定使用哪些 tools,而不是遵循固定 workflow。

避免过度工程化。Parallel patterns 会增加 orchestration overhead,只有对 long-running subtasks 才值得。Routing patterns 需要清晰 categories,当分类模糊时应避免使用。Orchestrator patterns 会通过多个 LLM calls 增加延迟。

See Also

Anthropic 的 guide to building effective agents 提供了 workflow patterns 和 agent design principles 概览。

Claude Cookbook on agent workflow patterns 包含 basic multi-LLM workflows 的代码示例。

8.3 选择 Agentic Framework

Problem

你正在构建第一个 agentic AI applications,并寻找最适合团队和 use cases 的 setup。

Solution

图 8-10 展示了一个按 abstraction level 对构建 agents 的 frameworks 和 platforms 进行分组的简单方式。

image.png

图 8-10:Framework abstraction levels:control versus convenience

表 8-1 总结了三个 abstraction levels。

表 8-1:Agentic tooling 的抽象层级

LevelExample tools/frameworksWhat defines the levelNotes(when to use)
1. No-code(极高抽象)Microsoft Copilot Studio、n8n、Zapier、Make带有预构建 connectors 且几乎不需要编码的 drag-and-drop workflow builders。适合 2 到 5 步的内部 workflows,以及没有工程资源的团队。帮助你快速开始。如果公司已经使用 Microsoft,Copilot Studio 可以很好融入生态。Pricing 通常基于 subscription,而不是 per token。
2. Agentic frameworks(code-first)OpenAI Agents SDK、LangGraph提供 agent primitives(tools、memory/persistence、tracing)以及构建 agentic applications 的 patterns 的 libraries。当你希望比从零构建更快开发,同时保留 code-level control 时使用。轻量选项,例如 OpenAI Agents SDK,可以最小化 lock-in,使后续切换 framework 或迁移到 from-scratch 方法更容易。
3. From scratch(最大控制)Direct model APIs、your own orchestration直接调用 LLM provider endpoints,并自己实现 orchestration、tools、state、memory 和 observability。当你需要最大灵活性或稳健性,并且团队具备工程能力来负责完整 stack,包括 logging、tracing 和 guardrails 时使用。

本章包含的 recipes 会描述如何用纯 Python 代码从零构建 agents,也会描述如何使用 LangGraph 和 OpenAI Agents SDK 等 agentic frameworks。No-code tools 不在本书覆盖范围内。因此,表 8-2 忽略 no-code tools,并总结了在 lightweight frameworks、high-abstraction frameworks 和 Python 从零构建之间选择的场景和建议。你可以将它作为参考。

表 8-2:选择 agentic framework:场景和建议

ScenarioFrom scratchLightweight frameworkHigh-abstraction frameworkNotes
你需要在数周内搭建 agentic RAG app 并投入生产。LangGraph 有很多内置功能,帮助你快速构建并发布方案。从零构建可能耗时更长,但可能更稳健。
你有许多低到中等经验的开发者维护 app。只有当团队经验丰富,并且 agent 形态非常清楚时,才从零构建。否则,framework 提供标准化方法,比让 junior developers 从零写高质量 infrastructure 更容易遵循成熟模式。
你正在构建服务数千到数百万用户的方案,并且需要 robustness 和 scalability。From-scratch implementations 或 lightweight frameworks 可以减少 dependencies。依赖更少通常意味着维护工作更少、可能出错的组件更少。
你需要高度灵活且可定制的 agent,专门适配某一具体需求。从零构建让你完全控制 tools 如何定义、如何以及何时交互。也可以为具体 use case 自由设计 application flow 和 state management。
你需要 real-time debugging、observability、transparency 和 audit/compliance logs。OpenAI Agents SDK 包含 OpenAI 网站上的 tracing view,可跟踪 agent 每一步。LangGraph 通过 LangSmith 提供类似能力。如果从零构建,通常需要自己实现 logging 和 tracing。

这个 recipe 不推荐具体 frameworks。相反,它提供基于团队和 use case 评估选项的标准。

Discussion

Framework 的 abstraction level 决定 control versus complexity。最低层级是直接调用 LLM APIs,并将 tools 实现为 Python functions。这最大化 transparency 和 debugging。当你需要可审计行为,并且团队 Python 能力强时,使用这一层。

OpenAI Agents SDK 等中层 frameworks 提供结构,但没有重抽象。它们强制 tool patterns,并处理 orchestration,同时仍然可检查。当你希望使用成熟模式但不想引入大量 dependencies 时,可以使用这些 frameworks。

LangGraph 等高层 frameworks 通过 graph structures 管理 state。它们捆绑 observability、type safety 和 coordination primitives。对于需要内置 debugging tools 的复杂 multiagent systems,可以使用这些 frameworks。

Microsoft Copilot Studio 等 no-code tools 使用可视化界面。当 time-to-market 优先于 customization,且团队缺少 Python 专业能力时,可以使用这些工具。你用控制力换速度。

早期应避免重型 frameworks。先从 direct API calls 或 lightweight frameworks 开始,以理解需求。每一层抽象都会遮蔽行为,使 debugging 更难。每个 dependency 都会扩大 security attack surface。

Framework lock-in 会随着时间累积。LangGraph 的 state management 会强制一种结构,后续很难迁移。OpenAI Agents SDK 通过更简单抽象最小化 lock-in。

See Also

Anthropic 的 guide to building effective agents 提供了使用 direct LLM API calls 实现 agents 的建议。

OpenAI Agents SDK documentation 提供了带最少 dependencies 的 lightweight agent framework 参考。

8.4 通过 Function Calling 构建 Agentic System

Problem

你想在不使用任何 framework 的情况下构建 agentic system。

Solution

使用 function-calling 方法。将 agent 可以使用的每个 tool 定义为 Python function,并向 LLM 提供一个 dictionary,其中包含这些 functions 的关键信息,使它可以判断当前任务应该使用什么。

在这种 setup 中,LLM 充当 planner,根据用户 query 规划一系列 tool calls。Python runtime 随后逐步执行这些 tool calls。图 8-11 展示了工作方式:LLM 只负责决策,代码负责执行。这个 recipe 实现了一个使用 Python functions 解决数学问题的 agent。该 agent 会将 15 + 23 * 4 这样的数学任务拆解成一系列较小步骤,并决定每一步应用哪个数学 function。

image.png

图 8-11:LLM 决定调用哪个 tool,Python 执行它

这个 recipe 使用 OpenAI 的一个 LLM 决定调用哪些 functions,以及调用顺序。稍后,系统还会访问天气 API,因此它不仅可以处理数学运算,也能处理 “What is the current weather?” 这样的 queries。

第一步,定义所需数学函数。在这个示例中,定义 add_numbers,使 agent 能够对两个数字求和。

实现本身只有一行,也就是 a + b,但 docstring 才是重要部分:它描述了函数行为、输入参数、返回值和数据类型,并包含示例。这个 docstring 成为 LLM 后续判断使用哪个 tool 的基础:

def add_numbers(a, b):
    """
    Tool for adding two numbers together.

    This tool takes two numbers as input and returns their sum. It can be
    used for:
    - Basic addition operations
    - Combining numeric values
    - First step in more complex calculations

    Args:
        a (float): The first number to add
        b (float): The second number to add

    Returns:
        float: The sum of the two input numbers (a + b)

    Example:
        add_numbers(5.0, 3.0) -> 8.0
        add_numbers(-1.0, 1.0) -> 0.0
    """
    return a + b

此外,recipe 还定义了 subtraction、multiplication 和 division 的数学函数。它们遵循相同模式,为了简洁这里省略。你可以在该 recipe 的 Jupyter notebook 中找到所有函数。

有了数学函数和前面 Recipe 8.1 中展示的 weather tool,系统就拥有了一组可以解决请求的 tools。

为了确保 LLM 理解可以使用哪些 functions,你必须将 function information 打包进 dictionary。这个 dictionary 包含:

  • Function name
  • Detailed description
  • Input parameters 及其 schema

填充 description field 的一种方便方式,是复用 function 的 docstring。图 8-12 展示了如何通过读取函数的 __doc__ attribute 来访问 docstring。

image.png

图 8-12:获取函数的 docstring

接下来,定义 function calling 所需的 tools dictionary。这个示例只展示 add_numbers 的第一项:

tools = [
  {
    "type": "function",
    "function": {
      "name": "add_numbers",
      "description": add_numbers.__doc__,
      "parameters": {
        "type": "object",
        "properties": {
          "a": {
            "type": "number",
            "description": "The first number to add"
          },
          "b": {
            "type": "number",
            "description": "The second number to add"
          }
        },
        "required": ["a", "b"]
      }
    }
  },
  ...
]

你会将 tools dictionary 与 LLM 一起使用,规划应该用哪些 tools 来回答用户问题。

为此,你使用下面代码调用 OpenAI endpoint,在 messages 中包含用户 query,并通过 tools parameter 传入 tools dictionary。这给了 LLM 选择最佳 tool 的信息。Response 包含一个 tool calls 列表。

代码最后部分会遍历 tool calls,找到对应函数,加载 LLM 指定的参数,然后通过解析真实 Python object 来调用 tool:

import openai
import json

user_query = "What is the result of (5.0 + 3.0) * 2.0?"
messages = [{"role": "user", "content": user_query}]

# Call the LLM with the available tools
response = openai.chat.completions.create(
    model="gpt-5-mini", messages=messages, tools=tools
)

# --- Extraction and Execution Logic ---
message = response.choices[0].message
tool_calls = message.tool_calls
finish_reason = response.choices[0].finish_reason

for tool_call in tool_calls:
    tool_name = tool_call.function.name
    # 1. Extract and parse arguments
    arguments = json.loads(tool_call.function.arguments)

    # 2. Locate the function object
    tool = globals().get(tool_name)

    # 3. Execute the function and capture the result
    result = tool(**arguments) if tool else {}

当你运行这个 snippet,会看到它只执行了一次 tool call,也就是括号内的第一步加法。第一次 tool call 的结果还没有回答用户问题,但它是正确的第一步。图 8-13 展示了 LLM response 的内容,其中包含被选择的 tool 以及输入参数值。

image.png

图 8-13:用于数学问题的 function calling:LLM 只解析了第一组括号

要解决完整问题,该过程必须在循环中运行。执行第一个 function 后,LLM 必须决定流程应如何继续。

下面代码将 selected function 的执行封装为一个 helper function,可以反复调用。随后,这个 helper 被用于一个 while loop 中,该 loop 持续调用 LLM;如果收到 tool calls,就执行 tool,将 tool result 追加到 messages 中,然后重复,直到模型返回最终答案。实现代码如下:

def process_function_invocations(invocations):
    """Processes LLM tool requests and executes the functions."""
    results = []
    for invocation in invocations:
        function_name = invocation.function.name
        function_args = json.loads(invocation.function.arguments)

        # Get and execute the function
        function = globals().get(function_name)
        # Execute the function with unpacked arguments
        function_result = function(**function_args) if function else {}

        # Format the result to be sent back to the LLM
        results.append({
            "role": "tool",
            "content": json.dumps(function_result), # Must be a string
            "tool_call_id": invocation.id,
        })
    return results

user_query = "What is the result of (6.7 + 3.3) * 2.0?"
messages = [{"role": "user", "content": user_query}]
finish_reason = "tool_calls" # Initialize to enter the loop

while finish_reason == "tool_calls":
    # Call the LLM with the conversation history and tools
    response = openai.chat.completions.create(
        model="gpt-5-mini",
        messages=messages,
        tools=tools
    )

    # Extract response details
    message = response.choices[0].message
    tool_calls = message.tool_calls
    finish_reason = response.choices[0].finish_reason

    # Add the LLM's response (tool request or final answer) to history
    messages.append(message)

    if finish_reason == "tool_calls":
        # Execute the tool and format the results for feedback
        tool_results = process_function_invocations(tool_calls)
        # Add the tool results to the history
        messages.extend(tool_results)

图 8-14 展示了 loop 完成后的 message list。它包含三项:用户问题、第一次 tool call 和第二次 tool call。

image.png

图 8-14:简单数学问题的 iterative function calls 示例

现在,用同样方法测试一个还需要天气信息的问题。这个 sample query 告诉 agent,用户几天后会去 London,并要求系统检索天气,同时计算距离返回还有多少天:

user_query = (
    "I'm planning a trip to London. "
    "I'll arrive in 5 days and stay for 6 days. "
    "Can you tell me what the current weather is in London, "
    "and also calculate in how many days I will return home from today?"
)

messages = [{"role": "user", "content": user_query}]

finish_reason = "tool_calls"  # Initialize finish_reason

while finish_reason == "tool_calls":
    print("messages:", messages)
    response = openai.chat.completions.create(
        model="gpt-5-mini",
        messages=messages,
        tools=tools
    )

    finish_reason = response.choices[0].finish_reason

    if finish_reason == "tool_calls":
        message = response.choices[0].message
        tool_calls = message.tool_calls
        results = process_function_invocations(tool_calls)

        messages.append(message)
        messages.extend(results)

final_content = response.choices[0].message.content
print("Final content:", final_content)

该 sample query 使用相同流程执行。如果你打印输出,会看到第一个 tool call 检索 London 当前天气,下一次 call 则计算距离返回还有多少天。

Discussion

LLMs 在 arithmetic 上并不可靠,因此 tool calling 会将计算委托给能够准确执行计算的 Python functions。Docstring 充当模型和 tool 之间的契约,描述函数做什么以及如何调用。

Function calling 支持 step-by-step agent workflows。模型决定使用哪个 tool,运行它,并基于结果进行 reasoning。由于每次 call 都是显式的,因此易于 inspection、logging 和 debugging。

当任务需要外部数据或计算时,使用 function calling。对于简单 one-shot questions,单次 LLM call 更简单、更快。

主要取舍是 latency:每次 tool call 都会增加一次模型 round trip。在性能敏感系统中,可以通过 batching 或 asynchronous execution 降低这一开销,如 Recipe 8.5 所示。

See Also

OpenAI function calling guide 提供了实现 function calling 的详细文档。

GitHub 上的 Agentic AI code examples 展示了构建 custom agents 的示例实现。

8.5 使用 asyncio 加速 Agents

Problem

你想加速那些受 I/O wait time 限制、而不是受 sequential dependencies 限制的独立 agent tasks。

Solution

使用 Python 的 asyncio module 并发运行 I/O-bound tasks。给函数定义添加 async,将其变成 coroutine,然后使用 await asyncio.gather 并发运行多个 coroutines。

在这个示例中,你定义两个 coroutines,通过不同长度的 sleep 来模拟 API calls:

import asyncio
import time

async def fetch_data():
    await asyncio.sleep(2)  # Simulating an API call that takes 2 seconds
    return {"status": "success", "data": "Some data from API"}

async def process_data():
    # Simulating data processing that takes 1 second
    await asyncio.sleep(1)
    return {"status": "complete", "result": "Processed data"}

async def main():
    start_time = time.time()
    # Run both coroutines concurrently;
    # total time ≈ longest single task (2s)
    data, result = await asyncio.gather(fetch_data(), process_data())
    end_time = time.time()

    print(f"Total time: {end_time - start_time:.2f} seconds")

asyncio.run(main())

图 8-15 展示了计时结果。使用 asyncio 后,总运行时间大约等于最长任务的耗时。整体时间从约 3 秒降低到约 2 秒。

image.png

图 8-15:使用 asyncio 并发运行任务

作为一个实践示例,设想一个 agentic chatbot 用于分析上传的 PDFs。第一步是数字化内容,并扫描特定值。扫描 PDF 本质上是一份由图像组成的长文档,因此每一页可以转换为图像,并由 multimodal model 处理以提取文本。这种方法强大但耗时。下面的实现会在单独 pages 之间并行处理。

图 8-16 展示了通过并发运行 tasks 从所有 pages 中提取内容的流程:

  1. Controller agent 将每一页发送给 multimodal model 以提取文本。
  2. 每个 multimodal-model call 并发运行,每次从一页中提取文本。
  3. Merger agent 收集并合并各页结果,形成连贯输出。

image.png

图 8-16:使用 asyncio 的 entity extraction workflow

定义实际重任务 coroutine:使用 multimodal model 从单张图像中提取 entities:

async def extract_entities_from_image(image, page_num):
    start_time = time.time()

    buffer = io.BytesIO()
    image.save(buffer, format="PNG")
    image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")

    client = openai.AsyncOpenAI()
    response = await client.chat.completions.create(
        model="gpt-5.2",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "Extract entities as JSON."},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/png;base64,{image_base64}"
                        },
                    },
                ],
            }
        ],
        max_tokens=1000,
    )

    # Write response to individual JSON file
    filename = f"../datasets/sample_data_asyncio/page_{page_num}_entities.json"
    with open(filename, "w") as f:
        f.write(response.choices[0].message.content)

    end_time = time.time()

    return response.choices[0].message.content

下一步会加载 PDF,将每一页转换成 image,并并发提取前三页文本。你可以选择并行处理的页数。限制因素通常是 API rate limit。

Model providers 通常会说明如何估算给定分辨率图像的 token usage。有了这些信息以及每张图像的处理时间,你就可以估算可以并发处理多少张图像而不超过 rate limits:

async def main():
    # Convert PDF to images
    images = convert_from_path(
        "../datasets/sample_data_asyncio/Laptop_Order_Invoice.pdf", dpi=200
    )
    # Process only first 3 pages for demo purposes
    end_page_to_process = 3
    results = await asyncio.gather(
        *[
            extract_entities_from_image(img, i + 1)
            for i, img in enumerate(images[:end_page_to_process])
        ]
    )

    # Merge the results to one JSON,
    # they are all from the same list of entities
    client = openai.AsyncOpenAI()
    response = await client.chat.completions.create(
        model="gpt-5.2",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": "Merge these JSON lists into one."
                        + "They are all from the same invoice.",
                    },
                    {
                        "type": "text",
                        "text": "\n".join(results),
                    },
                ],
            }
        ],
    )

    merged_json = response.choices[0].message.content
    return merged_json

图 8-17 展示了生成的 dictionary。代码会把每张图像提取出的 values 保存为 JSON 文件,并将结果合并成一个单一 dictionary。

image.png

图 8-17:以 JSON 形式保存的 extracted entities

为了验证 asyncio 是否提升性能,可以记录执行时间。在这个示例中,第 1 页耗时 19 秒,第 2 页耗时 34 秒,第 3 页耗时 38 秒。整个脚本总耗时 49 秒。如果没有 asyncio,顺序处理至少需要 90 秒。

Discussion

Agent systems 会对 LLMs、embedding models 和 vision models 发起多个 API calls。每次调用都需要数秒。Python 的 asyncio 会在 I/O waits 期间暂停 tasks,并切换到其他 ready tasks。顺序执行需要所有 durations 的总和。使用 asyncio 后,总时间大约等于最长任务耗时。

当 agent 会查询多个独立 data sources、并行调用多个 tools,或同时为多个 chunks 生成 embeddings 时,可以使用 asyncio。并发获取三个 data sources 的耗时等于最慢一次调用,而不是三者之和。

当 workflow 是 sequential 的,也就是每一步依赖前一步结果时,避免使用 asyncio。对于 API calls 很少的简单 scripts,也应避免使用,因为额外 overhead 会增加复杂性而没有明显收益。

取舍是代码复杂性。使用 asyncio 的异步编程,需要理解 coroutines、event loops 和 await semantics。Debugging 更困难,因为错误可能发生在多个同时运行的 tasks 中,难以追踪是哪一个 task 导致问题。Error handling 也必须考虑 partial failures。

对于 response time 很重要的 user-facing agents,应使用 asyncio。等待多个 API calls 顺序完成会让用户沮丧;asyncio 通过并发运行独立任务减少等待时间。

See Also

Python asyncio documentation 是关于 asynchronous I/O、event loops 和 coroutines 的官方参考。

Real Python 的 asyncio tutorial 提供了 Python asyncio 并发编程的动手指南。

8.6 使用 OpenAI Agents SDK 和 Chroma 构建 Sales Negotiation Agent

Problem

你想使用轻量级 agent framework 构建 sales assistant agent,这样无需从零构建所有内容,同时仍然拥有充分灵活性。

Solution

OpenAI Agents SDK 是一个用于构建 agents 的轻量级 framework。它覆盖最重要概念,例如如何定义 guardrails、agents 如何将任务 hand off 给彼此,以及如何逐步 trace agent 行为。

这个 recipe 构建了一个代表你进行谈判的 sales assistant agent。稍后在 recipe 中,会加入 customer agent 和 moderator agent。Moderator 会交替触发 customer 和 salesman agents。Salesman agent 可以访问一个 vector store,其中包含过去与客户聊天的信息、pricing 和 manufacturing costs。

图 8-18 展示了包含三个 agents 的完整 setup,以及它们如何彼此交互。

image.png

图 8-18:要构建的 agent 的 agentic workflow chart

要运行这个 recipe 中的代码,需要 OpenAI Agents SDK library。Agent 使用 Chroma vector database 将 pricing 和 historical sales information 作为 long-term memory 存储。安装 library 和 database:

pip install openai-agents chromadb

要运行 pdf2image library,还需要额外系统工具。在 Linux 上,大多数发行版都包含 Poppler utilities。如果没有,可以用 package manager 安装:

sudo apt-get install poppler-utils

在 Windows 上,可以从官方 Poppler repository 下载 Poppler binaries,并将其加入 system PATH,使 pdf2image library 可以找到它们。

第一步是设置 vector database,它将作为 sales agent 的 long-term memory。这个 recipe 使用 Chroma vector database,并用 salesperson 和 potential buyer 之间 email chain 的 sample emails 填充它。Email history 还包含业务信息,例如 retail prices、common manufacturing costs 和 product details。这为 sales agent 提供额外上下文和更大的谈判空间。

首先,创建一个 Chroma vector database,以及名为 email_history 的 collection。通过加载 email history 文本文件、将其拆分为 chunks、embedding 这些 chunks 并上传到 vector database 来填充 collection:

import os
import shutil
import chromadb
from chromadb.config import Settings

def fill_chroma_db():
    chroma_dir = "./chroma_email_history_db"

    # If the directory exists, remove it to overwrite
    if os.path.exists(chroma_dir):
        shutil.rmtree(chroma_dir)

    client = chromadb.Client(
        Settings(persist_directory=chroma_dir)
    )
    collection = client.get_or_create_collection("email_history")

    with open(
        "../datasets/sample_data_negotiation_agents/sample_email_history.txt",
        "r",
        encoding="utf-8"
    ) as f:
        email_text = f.read()

    email_docs = [
        e.strip()
        for e in email_text.split("\n---\n")
        if e.strip()
    ]

    for idx, doc in enumerate(email_docs):
        collection.add(
            documents=[doc],
            ids=[f"email_{idx+1}"]
        )
        print(f"Added email_{idx+1}")


fill_chroma_db()

接下来,通过创建一个可以搜索 email_history collection 的 tool,使 agent 可以查询 vector database。在这个示例中,定义名为 query_database 的 tool,它使用 Chroma 查询 collection。它连接到 email_history,并基于输入文本提交 query。

使用 OpenAI Agents SDK 时,要将标准 Python function 转换为 tool,需要在函数定义上方添加 @tool decorator。

Python 中的 decorator 是一种特殊函数,会修改另一个函数的行为。当你将 decorator 应用于某个函数时,它会包裹原始函数,并用额外功能增强它。代码如下:

import chromadb
from chromadb.config import Settings

@function_tool
def query_database(query_text, n_results=3):
    """
    Query emails using semantic search from a local Chroma collection.
    """

    # Set up Chroma client and collection
    chroma_dir = "./chroma_email_history_db"
    client = chromadb.Client(Settings(persist_directory=chroma_dir))
    collection = client.get_or_create_collection("email_history")

    # Perform the query
    results = collection.query(
        query_texts=[query_text],
        n_results=n_results,
        include=["documents"],
    )
    return {
        "query": query_text,
        "documents": (
            results["documents"][0] if results["documents"] else []
        ),
    }

接下来,定义 sales agent。这个 agent 可以访问刚创建的 tool,用于与客户谈判。

代码导入所需 modules,并使用 Agent class 创建新 agent。创建 agent 时,需要提供 name、instructions、它可以访问的 tools,以及它使用的 LLM:

from agents import Agent, Runner
from agents import trace

# Create a negotiation agent with email search capability
salesman_agent = Agent(
    name="Negotiation Agent",
    instructions="""You are a skilled negotiation agent
    representing a salesperson.

    Your role:
    - Analyze the current email conversation history
    - Search for relevant information using the query_database tool
    - Generate informed responses with strategic counter-offers

    Process:
    1. Use query_database to find relevant context from email history
    2. Analyze customer concerns and pricing constraints
    3. Craft a professional response with a competitive counter-offer
    4. Maintain a collaborative tone while protecting profit margins
    """,
    tools=[query_database],
    model="gpt-5.2"
)

# Helper function to run the negotiation agent
async def negotiate_with_customer(email_history):
    with trace("Negotiation Response"):
        result = await Runner.run(
            salesman_agent,
            f"Email conversation history:\n{email_history}"
        )
    return result.final_output

现在,通过提供已有 email history 测试 agent。Email history 包含 customer 与 salesperson 之间的 email chain:

  • Customer 想购买 15 台笔记本电脑,每台不超过 €950,尽管零售价是 €1,299。
  • Salesperson 提供 12% discount。
  • Customer 回复说 discount 不够,并声称可以从另一家供应商以 €1,000 获得类似笔记本。
  • 讨论目前停在这里。

将这个 email history 提供给 agent,让它继续谈判:

# Example: Using the negotiation agent
import asyncio
from agents import trace


async def demonstrate_negotiation():
    with open(
        "../datasets/sample_data_negotiation_agents/sample_email_history.txt",
        "r",
        encoding="utf-8"
    ) as f:
        email_history = f.read()

    # Generate negotiation response using the agent
    with trace("Automated Customer Response"):
        response = await negotiate_with_customer(email_history)

    print(response)
    return response


await demonstrate_negotiation()

到目前为止,这只在一个方向上工作。为了模拟完整谈判,需要定义一个 customer agent,代表客户回复。

此外,还要定义 moderator agent。Moderator 通过在 customer 和 salesperson 之间交替回合,促进谈判。

为了让 moderator 可以触发其他 agents,代码使用 AsTool function 将每个 agent 包装为 tool,并提供 tool description。这给 moderator 提供两个 tools:

  • 一个 customer-agent tool,用来调用 customer agent。
  • 一个 salesperson-agent tool,用来调用 salesperson agent。

下面是定义 moderator,并分别调用 salesperson 和 customer agents 的完整代码:

from agents import Agent, Runner

async def run_negotiation_process():
    """
    Orchestrates a negotiation between customer and salesperson agents
    using a moderator agent to facilitate the conversation.
    """
    # Load the email history from file
    with open(
        "../datasets/sample_data_negotiation_agents/sample_email_history.txt",
        "r",
        encoding="utf-8"
    ) as f:
        email_history = f.read()

    # Define the customer agent
    customer_agent = Agent(
        name="Customer",
        instructions="""Negotiate for the best laptop deal.
                       Be polite but persistent.
                       Respond to the email chain below.""",
        model="gpt-5-mini",
    )

    # Convert agents to tools for the moderator
    customer_agent_tool = customer_agent.as_tool(
        tool_name="customer",
        tool_description=(
            "Tool that represents the customer in negotiations."
        )
    )

    salesman_agent_tool = salesman_agent.as_tool(
        tool_name="salesman",
        tool_description=(
            "Tool that represents the salesperson in negotiations."
        )
    )

    # Define the moderator agent with clear instructions
    moderator_instructions = """
    You are a moderator facilitating negotiation between
    customer and salesperson.

    Process:
    1. Receive email history with the last message from customer
    2. Use salesman_agent_tool to generate salesperson response
    3. Append response to email history
    4. Use customer_agent_tool to generate customer response
    5. Continue alternating until agreement or breakdown

    Rules:
    - Aim for mutually beneficial agreement
    """

    moderator_agent = Agent(
        name="Moderator",
        instructions=moderator_instructions,
        model="gpt-5-mini",
        tools=[customer_agent_tool, salesman_agent_tool]
    )

    # Execute the negotiation with tracing
    with trace("Negotiation Process"):
        responses = await Runner.run(
            moderator_agent,
            f"Begin negotiation with this email history: {email_history}"
        )

    return responses

如果运行完整代码,moderator 会交替调用 customer-agent tool 和 salesperson-agent tool。对话会持续,直到双方达成 agreement 或达到最大迭代次数。

如果你使用 OpenAI Agents SDK 与 OpenAI API endpoint,可以在 OpenAI account 中查看 traces。该界面会显示每个 agent call 的 input、output 和耗时。

图 8-19 展示了 OpenAI platform 中 negotiation workflow 的 trace view,其中 salesperson、customer 和 negotiation agents 交互,并调用 database queries 等 tools。你可以看到步骤顺序和每个 action 的耗时。在这个示例中,customer 审查更新后的 invoice 并批准,确认可以扣款,从而成功结束谈判。

image.png

图 8-19:OpenAI Agents SDK trace,展示 customer 和 salesman agents 之间的 negotiation process

Discussion

OpenAI Agents SDK 是一个用于构建 agents 的轻量级 framework。它覆盖 guardrails、agent hand-offs 和 step-by-step tracing。在 Python 中,decorator 会修改函数行为。@function_tool decorator 会将函数转换为 agents 可以调用的 tool。

SDK 提供内置 tracing、guardrails 和 controls,以防止 endless loops。你可以在 OpenAI Platform Traces 审查 traces。每次 run 都会作为单独 entry 记录,显示完整执行步骤。图 8-20 展示了 agent trace dashboard,你可以在其中监控单个 agent runs,并检查其完整执行历史。

image.png

图 8-20:通过 OpenAI agent trace,你可以监控和可视化 agent actions、tool calls 和 model usage

SDK 会将 agents 包装成其他 agents 可以调用的 tools,从而支持 hierarchical delegation。Moderator pattern 协调具有不同目标但共享 conversation context 的 specialized sub-agents。每个 agent 都暴露一个简单接口,即 input text、output text,因此它们可以互换,也可以独立测试。

对于 negotiations、support routing 或 collaborative editing 这类 multiparty interactions,如果不同 roles 需要不同 objectives,可以使用这个 pattern。Moderator 会将 conversation history 传递给每个 sub-agent。

当 latency 至关重要时,避免使用这种 pattern。每次 agent hand-off 都需要完整 LLM call,而多轮对话会线性累积成本。对于没有对话的简单任务委派,direct function calls 或 routing patterns 更高效。

轻量和重量 frameworks 之间的取舍,在规模扩大时会变得明显。OpenAI Agents SDK 提供最少 abstractions,但要求你自己实现 coordination logic、error handling 和 state management。LangGraph 会自动管理 shared state,但需要理解 graph-based execution models。现成 OpenAI tools 能减少开发时间,但会引入 vendor coupling。

TIP

你可能听说过 agent framework Swarm。Swarm 也是 OpenAI 的项目,曾作为 lightweight multiagent orchestration framework。不过,它已经被 OpenAI Agents SDK 完全替代。如果你以前使用 Swarm 搭建 agent setup,建议迁移到 OpenAI Agents SDK。

See Also

OpenAI Agents SDK documentation 提供综合指南,包括 agent hand-offs 和 advanced patterns。

8.7 使用 MCP Tools 丰富 Agent 能力

Problem

你想使用别人开发并以 MCP server 形式共享的 tools,增强并扩展 agents 的 toolkit。

Solution

这个 recipe 构建一个 cooking-assistant agent,它会在 web 上搜索 recipes、分析它们并保存它们。为了浏览 web,代码使用 Playwright MCP server 控制 web browser。

WARNING

一些 MCP servers 在 Windows 上运行不顺畅,尤其是在 Jupyter Notebook 中。如果遇到太多错误,可能需要切换到 Linux。借助 Microsoft Windows Subsystem for Linux(WSL),你可以直接在 Windows 机器上安装 Linux subsystem。要在 Windows 10 或 11 上设置 WSL,打开 PowerShell 并运行 wsl --install,默认会安装 Ubuntu。详情见 Microsoft 网站上的安装指南。

MCP servers 存在于许多流行语言中,包括 Go、Java、Kotlin、C#、PHP、Node.js 和 Python。这个 recipe 使用基于 Node.js 的 Playwright tooling,因此你需要 Node.js 和 Node Package Manager(NPM)。如果改用 Python-based MCP server,则需要 Python 和 pip。

你还需要一个 package-execution tool,可以在不永久安装的情况下运行 scripts 和 packages。对于 Python-based servers,通常是 uvx;对于 JavaScript-based tools,是 npx。MCP ecosystem 提供了大量 servers,覆盖许多类别,包括 filesystem operations、web browsing、data access 和各种 integrations,如图 8-21 所示。

image.png

图 8-21:MCP servers 和 tools 示例

在这个 recipe 中,MCP client 是使用 OpenAI Agents SDK 构建的 agent。要使用它,安装 OpenAI Agents package:

pip install openai-agents

Recipe 的其余部分展示该 agent 如何连接到多个 MCP servers。要查看 NPM registry 中可用 MCP servers,运行以下命令:

npm search @modelcontextprotocol

你会得到一个可用 servers 列表。图 8-22 展示了一个例子:Figma MCP server。

image.png

图 8-22:可用 MCP servers 列表

这个 recipe 使用 Playwright MCP server,它是最受欢迎的 MCP tools 之一。安装 MCP server,更多说明见 Playwright MCP GitHub repo:

# Using npm
npm install -g @modelcontextprotocol/server-playwright

# Or using the community version
npm install -g @playwright/mcp@latest

接下来,配置 MCP client 使用刚安装的 MCP server。Client 使用 OpenAI Agents SDK 中的 MCP server wrapper class 连接到 Playwright MCP server。参数指定 agent 被允许使用哪个 server。当执行 snippet 时,它会列出该 MCP server 提供的所有 tools,并将它们存入 tools 变量:

import asyncio
from agents.mcp import MCPServerStdio


async def connect_to_playwright():
    # Define connection parameters
    # "npx" runs Node.js packages,
    # "@playwright/mcp@latest" specifies the package
    params = {"command": "npx", "args": ["@playwright/mcp@latest"]}

    # Create server connection with extended timeout
    # Increased from default 5s
    async with MCPServerStdio(
        params=params, client_session_timeout_seconds=30
    ) as server:
        # List available tools from the server
        tools = await server.list_tools()
        print("Available tools:", tools)

# Run the async function
if __name__ == "__main__":
    asyncio.run(connect_to_playwright())

图 8-23 展示了结果。tools object 是一个 dictionary,其中包含 tools 列表。例如,一个 tool 关闭浏览器,另一个调整浏览器尺寸,另一个上传文件,等等。一般来说,大多数浏览器操作都会作为描述清晰的 tools 暴露。

image.png

图 8-23:Playwright MCP server tools

下一步是用 Playwright tools 做一些有用的事情。你会定义一个 research agent,它使用 MCP server、MCP Playwright server 和 MCP filesystem server 来保存文件。

指示 agent 可以使用 Playwright MCP server 的 tools 浏览网站,并将信息保存到文件。要求 agent 找到一份 chocolate chip cookie recipe,并将 recipe 保存到新的 Markdown 文件:

import asyncio
from agents.mcp import MCPServerStdio
from agents import Agent, Runner

async def create_cookie_research_agent():
    # MCP server configurations
    files_params = {
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
    }
    browser_params = {"command": "npx", "args": ["@playwright/mcp@latest"]}

    # Agent instructions
    instructions = "You can browse websites and save information to files."

    async with MCPServerStdio(
        params=files_params, client_session_timeout_seconds=60
    ) as files:
        async with MCPServerStdio(
            params=browser_params, client_session_timeout_seconds=60
        ) as browser:

            # Create agent
            agent = Agent(
                name="research_agent",
                instructions=instructions,
                model="gpt-5-mini",
                mcp_servers=[files, browser],
            )

            # Run task
            result = await Runner.run(
                agent,
                "Find a chocolate chip cookie recipe and "
                "save it to recipe.md"
            )

            print(result.final_output)


if __name__ == "__main__":
    asyncio.run(create_cookie_research_agent())

一旦运行脚本并成功完成,你应该能在脚本所在目录中看到一个新的 Markdown 文件,其中保存了 chocolate chip cookie recipe。

Discussion

Model Context Protocol(MCP)标准化了 LLMs 与 tools 的交互方式。就像 HTTP 之于浏览器,MCP 让任何 MCP-compatible agent 可以使用任何 MCP-compatible tool,而无需自定义集成代码。这解决了 tool integration 问题:如果没有 MCP,每个 framework 都需要自己的 tool format。

MCP 有三个组成部分:client、server 和 protocol。Client 是你的 agent application。Servers 提供 tools 和 data sources,可以本地运行,也可以远程运行。你可以为自己的 tools 构建 custom MCP servers。图 8-24 展示了该架构:MCP clients,例如 custom agents 或 Claude,通过标准化 MCP 连接到 local servers,例如 Playwright、FileSystem 和 custom tools,以及 remote servers,例如 GitHub、Stripe 和 Slack。

image.png

图 8-24:在 MCP 架构中,clients 连接本地或远程 servers,以访问 tools 和 services

当你要构建跨项目或团队共享的 tools 时,可以使用 MCP。作为 MCP server 的 database query tool 可以与任何支持 MCP 的 agent framework 配合使用。你只需要更新 MCP server 一次,而不是维护多个 framework-specific versions。

对于 tools 与 agent logic 紧密耦合的简单 single-purpose agents,应避免使用 MCP。只有当 tool reusability 或 cross-framework compatibility 重要时,protocol overhead 才值得。

取舍是额外 infrastructure。MCP servers 作为独立进程运行,需要 deployment、monitoring 和 network-level error handling。Agent 必须处理 unreachable、timeouts 和 malformed responses。

MCP 的安全性与 local function calls 不同。MCP servers 接受 network requests,因此需要 authentication、authorization 和 input validation。每个 MCP server 都会扩大 attack surface。

See Also

Model Context Protocol documentation 提供了构建 MCP servers 和 clients 的官方指南。

OpenAI Agents SDK MCP integration guide 提供了在 OpenAI Agents SDK 中使用 MCP 的说明。

OpenAI Cookbook MCP tutorials 包含 MCP 实现的实践 walkthroughs。

MCP Servers directory 收集了可集成的 MCP servers。

8.8 使用 LangGraph 构建 Agentic System

Problem

你想构建一个复杂 agent,它能够 planning、调用 tools、观察结果,并循环直到完成,同时你希望通过定义 nodes、edges 和 stop conditions 来控制执行。

Solution

这个 recipe 创建一个 assistant,它可以提供天气信息,并基于天气建议从 A 到 B 的最佳交通方式,以及应该穿什么衣服。

要在 LangGraph 中定义这个 agentic system 的 flow,需要使用三个核心组件:state、nodes 和 edges。State 跟踪对话,nodes 执行动作,edges 控制流程。

要运行代码,需要 LangGraph library。此外,weather API 示例使用 GeoPy 解析坐标。随后使用 Requests library 调用天气 API。安装这些 dependencies:

pip install langgraph geopy requests langchain_openai langchain-core pydantic

对于这个 agent,state 会将你和 agent 之间的 ongoing conversation history 存储为 messages 列表。每个 node 之后,state 都会更新。LangGraph 提供一个名为 add_messages 的 helper,用于通过追加方式更新 state object 中的 messages list。该函数像 reducer 一样工作:它接收当前 list 和新 messages,并返回合并后的 list。

首先定义 agent 的 initial state,其中包含一个空 messages list:

from typing import Annotated,Sequence, TypedDict

from langchain_core.messages import BaseMessage
# Helper function to add messages to the state
from langgraph.graph.message import add_messages


class AgentState(TypedDict):
    """The state of the agent."""
    messages: Annotated[Sequence[BaseMessage], add_messages]
    number_of_steps: int

接下来,使用 LangChain tools library 将 functions 转换为 tools。Tool inputs 被定义为 Pydantic model。

你会定义两个 tools:

  • 一个让 agent 获取当前 weather forecast 的 tool。
  • 第二个接收 audio input,也就是 spoken user instructions,并转录它的 tool。

在转换为 tools 的函数 docstrings 中,简要描述函数做什么,以便 agent 将用户问题映射到正确 tool。Pydantic model 中预期字段也应做同样说明:

from langchain_core.tools import tool
from geopy.geocoders import Nominatim
from pydantic import BaseModel, Field
import requests

geolocator = Nominatim(user_agent="weather-app")

class SearchInput(BaseModel):
    location: str = Field(
        description="The city and state, e.g., San Francisco"
    )
    date: str = Field(
        description="the forecasting date for when to get "
        "the weather format (yyyy-mm-dd)"
    )

@tool("get_weather_forecast", args_schema=SearchInput, return_direct=True)
def get_weather_forecast(location, date):
    """Retrieves the weather using Open-Meteo API for a given
    location (city) and a date (yyyy-mm-dd).
    Returns a dict mapping time -> temperature for each hour.
    """
    location = geolocator.geocode(location)
    if location:
        try:
            url = "https://api.open-meteo.com/v1/forecast"
            params = {
                "latitude": location.latitude,
                "longitude": location.longitude,
                "hourly": "temperature_2m",
                "start_date": date,
                "end_date": date,
            }
            response = requests.get(url, params=params)
            data = response.json()
            hourly = data.get("hourly", {})
            times = hourly.get("time", [])
            temps = hourly.get("temperature_2m", [])
            result = {}
            for t, temp in zip(times, temps):
                result[t] = temp
            return result
        except Exception as e:
            return {"error": str(e)}
    else:
        return {"error": "Location not found"}

class AudioInput(BaseModel):
    audio_path: str = Field(
        description="Path to the audio file to transcribe"
    )

@tool("transcribe_audio", args_schema=AudioInput, return_direct=True)
def transcribe_audio(audio_path):
    """Transcribe audio file into text using OpenAI Whisper."""
    try:
        with open(audio_path, "rb") as audio_file:
            transcript = openai.Audio.transcribe(
                model="whisper-1",
                file=audio_file
            )
            return transcript.text
    except Exception as e:
        return {"error": str(e)}

tools = [get_weather_forecast, transcribe_audio]

LLM 会充当 orchestrator,它知道 agent 可以访问哪些 tools,并决定调用哪个 tool 以及使用什么参数。

在这个示例中,你将 weather-forecast tool 绑定到 LLM。如果用询问 Berlin 某天的天气这样的 query 调用模型,LLM 可以推断 weather tool 需要 location 和 date 作为输入。

对于这个 Berlin 示例,你不需要额外函数将 “Berlin” 转换为 geolocation,因为模型知道 Berlin 的坐标。LLM 会用正确参数调用 tool,并返回结果。使用以下代码初始化和绑定:

from datetime import datetime
from langchain_openai import ChatOpenAI

# Create LLM class
llm = ChatOpenAI(
    model="gpt-5.2",
    temperature=1.0,
    max_retries=2,
)

# Bind tools to the model
model = llm.bind_tools([get_weather_forecast])

# Test the model with tools
query = f"What is the weather in Berlin on {datetime.today()}?"
res = model.invoke(query)

print(res)

下一步,定义 nodes 和 edges。每个 node 被定义为 Python function。在这个示例中,一个 node 调用 LLM,另一个 node 调用选定 tool。两个 nodes 都接收 agent state 作为输入。

你还需要定义一个 edge function,用来决定 flow 是否应该继续。在这个例子中,它检查 message list 中最后一条 message 是否包含 tool_calls。如果最后一条 message 不是 tool call,运行结束:

from langchain_core.messages import ToolMessage
from langchain_core.runnables import RunnableConfig

tools_by_name = {}
for _t in tools:
    tools_by_name[_t.name] = _t

# Define our tool node
def call_tool(state: AgentState):
    outputs = []
    # Iterate over the tool calls in the last message
    for tool_call in state["messages"][-1].tool_calls:
        # Get the tool by name and arguments in smaller steps
        # to keep lines short
        tool_name = tool_call["name"]
        tool_args = tool_call["args"]
        tool_obj = tools_by_name[tool_name]
        tool_result = tool_obj.invoke(tool_args)
        outputs.append(
            ToolMessage(
                content=tool_result,
                name=tool_name,
                tool_call_id=tool_call["id"],
            )
        )
    return {"messages": outputs}

def call_model(
    state: AgentState,
    config: RunnableConfig,
):
    # Invoke the model with the system prompt and the messages
    response = model.invoke(state["messages"], config)
    # Return a list, because this will get added to the
    # existing messages state using the add_messages reducer
    return {"messages": [response]}


# Define the conditional edge that determines whether to continue or not
def should_continue(state: AgentState):
    messages = state["messages"]
    # If the last message is not a tool call, then finish
    if not messages[-1].tool_calls:
        return "end"
    # Default to continue
    return "continue"

到这里,你已经有了将 agent 构建为 graph 所需的所有组件。

下面代码创建一个新 graph,添加 nodes,并设置 entry point。然后添加 edge,它会在 LLM step 之后被评估。

每次 LLM call 后,should_continue edge 会检查 LLM 是决定下一步调用 tool,还是已经生成最终 response。你也会使用 normal edge 将 tools 连接回 LLM,以继续循环:

from langgraph.graph import StateGraph, END

# Define a new graph with our state
workflow = StateGraph(AgentState)

# 1. Add our nodes
workflow.add_node("llm", call_model)
workflow.add_node("tools",  call_tool)
# 2. Set the entrypoint as `agent`, this is the first node called
workflow.set_entry_point("llm")
# 3. Add a conditional edge after the `llm` node is called.
workflow.add_conditional_edges(
    # Edge is used after the `llm` node is called.
    "llm",
    # The function that will determine which node is called next.
    should_continue,
    # Mapping for where to go next, keys are strings from the
    # function return, and the values are other nodes.
    # END is a special node marking that the graph is finish.
    {
        # If `tools`, then call the tool node.
        "continue": "tools",
        # Otherwise finish.
        "end": END,
    },
)
# 4. Add a normal edge after `tools` is called, `llm` node is called next.
workflow.add_edge("tools", "llm")

# Now you can compile and visualize the graph
graph = workflow.compile()

由于 agent 被定义为 graph,你也可以打印它,以获得 execution flow 和 decision points 的概览:

from IPython.display import Image, display

display(Image(graph.get_graph().draw_mermaid_png()))

如图 8-25 所示,该 graph 包含两个 nodes:llmtoolsllm node 调用 LLM,tools node 调用选定 tool。Edges 决定执行是结束还是继续。如果继续,tool result 会反馈给 LLM。

image.png

图 8-25:可视化 LangGraph agent

要运行 agent,定义一个 inputs dictionary,并启用 streaming 调用 agent,以打印 intermediate steps:

from datetime import datetime

# Create our initial message dictionary
inputs = {
    "messages": [
        ("user", f"What is the weather in Berlin on {datetime.today()}?")
    ]
}

# Call our graph with streaming to see the steps
for state in graph.stream(inputs, stream_mode="values"):
    last_message = state["messages"][-1]
    last_message.pretty_print()

图 8-26 展示了 agent run 打印出的步骤。首先,用户询问 Berlin 天气。LLM 决定调用 weather-forecast tool,并传入 location Berlin 和 2025 年 10 月的某个日期。Tool 执行后,将结果返回给 LLM,LLM 解释结果并生成最终 response。由于最终 response 不包含另一个 tool call,运行结束。

image.png

图 8-26:LangGraph agent run output

如果想让 agent run 继续,可以向 state 变量追加 follow-up user question,这会自动触发 graph 重新运行:

state["messages"].append(("user", "Would it be warmer in Munich?"))

for state in graph.stream(state, stream_mode="values"):
    last_message = state["messages"][-1]
    last_message.pretty_print()

Discussion

LangGraph 将 agent control flow 定义为包含三个核心组件的 graph:

State

跟踪正在发生的事情,并保存共享数据。

Nodes

是执行工作的 functions:它们接收当前 state,执行 action,并返回更新后的 state。

Edges

连接 nodes,并决定下一个运行哪个 node。

图 8-27 展示了 ReAct pattern。Router 是一个 LLM,它决定是否调用外部 tool。

image.png

图 8-27:Chat with bound tools

LangGraph 让 agent state 和 control flow 显式化。你可以根据当前 state 精确定义下一个执行哪个 node。Edges 定义有效 transitions,防止 undefined states 或 infinite loops。

当你需要理解复杂、多步行为时,可以使用 LangGraph。Graph visualization 展示了可能执行路径,以及 agent 实际走了哪条路径。这有助于 debug 意外决策。当协调多个共享上下文的 agents 时,state management 会很有价值。LangGraph 会自动维护 shared state。

LangChain ecosystem 包含 LangGraph 用于 workflows、LangChain 用于 orchestration,以及 LangSmith 用于 observability。图 8-28 展示了这些组件之间的关系。

image.png

图 8-28:LangGraph versus LangChain versus LangSmith:关键差异一览

对于简单线性 workflows,避免使用 LangGraph。如果 agent 执行的是不带 branching logic 或 shared state 的固定序列,graph abstraction 会增加复杂性,却没有收益。

取舍是 framework coupling。LangGraph 的 state management 和 graph execution 会深度嵌入你的代码架构。迁移意味着必须重新实现整个 state flow 和 transition logic。LangSmith 提供捆绑式 observability,但会增加 ecosystem coupling。

See Also

LangChain Academy LangGraph course 提供关于 state、memory 和 deployment 的综合模块。

Gemini API LangGraph tutorial 提供了在 Gemini API 中使用 LangGraph 的指南。