Soup's Blog

Back

Agent实战(二)构建智能体的完整实践指南:工具调用、错误处理、动态模型选择与记忆扩展Blur image

本章节参考博客 代码开源Github

在大模型应用开发中,智能体(Agent) 是连接语言模型与现实世界能力的核心桥梁。它不仅能推理,还能调用工具、处理错误、动态切换模型、记住用户上下文,甚至按需返回结构化数据。本文将通过一系列递进式实验,初步了解 LangChain/LangGraph 中 Agent 的高级用法。


1. 基础 Agent 与工具调用#

首先创建一个基础 Agent,并为其配备三个工具:

  • 网络搜索(MyWebSearchTool
  • RAG 检索(RAGTool
  • 一个简单的除法函数 divide

然后发起一个涉及计算的问题:

state = {"messages": [{"role": "user", "content": "湖南省和河南省各有多少人口?它们之间的人口比例是多少?"}]}
for event in agent.invoke(state)['messages']:
    event.pretty_print()
python

结果


2. 自定义工具错误处理#

工具执行可能失败(如除零错误)。我们可以通过中间件拦截异常并返回友好提示。

定义错误处理中间件:#

from langchain.agents.middleware import wrap_tool_call
from langchain_core.messages import ToolMessage

@wrap_tool_call
def handle_tool_errors(request, handler):
    try:
        return handler(request)
    except Exception as e:
        return ToolMessage(
            content=f"Tool error: Please check your input and try again. ({str(e)})",
            tool_call_id=request.tool_call["id"]
        )
python

模拟错误工具:#

@tool('divide_error', parse_docstring=True)
def divide_error(a: int, b: int) -> int:
    return 1 / 0  # 故意制造错误
python

创建带错误处理的 Agent:#

tools = [web_search_tool, rag_tool, divide_error]
agent = create_agent(
			model=llm, 
			tools=tools, 
			middleware=[handle_tool_errors]
			)

# 调用
for event in agent.invoke(state)['messages']:
    event.pretty_print()
python

结果
divide_error 被调用时,Agent 收到的是我们自定义的错误消息,而非原始异常堆栈。

可以看到上面打印的输出,这是我们之前在中间件中定义的输出格式,Tool error: Please check your input and try again.


3. 动态模型选择:根据上下文切换 LLM#

有时我们需要根据用户需求切换底层模型(如 Qwen vs DeepSeek)。

定义上下文类:#

from dataclasses import dataclass
from typing import Literal

@dataclass
class CustomContext:
    provider: Literal["qwen", "deepseek"]
python

实现模型选择中间件:#

@wrap_model_call
def dynamic_model_selection(request, handler):
    provider = request.runtime.context.provider
    print(f"获取到provider={provider}")
    request.model = qwen_model if provider == "qwen" else deepseek_model
    return handler(request)
python

调用时指定模型:#

result = agent.invoke(
    {"messages": [{"role": "user", "content": "你是哪个公司下面的AI?"}]},
    context=CustomContext(provider="deepseek")
)
python

效果
Agent 将使用 DeepSeek 模型回答,而非默认的 Qwen。


4. 动态系统提示词:个性化交互风格#

我们可以根据用户角色(如“专家”或“初学者”)动态调整系统提示。

@dynamic_prompt
def user_level_prompt(request: ModelRequest) -> str:
    level = request.runtime.context.get("level", "beginner")
    base = "你是一个精通机器学习方面的专家."
    if level == "expert":
        return f"{base} 解释问题时提供更多的细节."
    else:
        return f"{base} 用小朋友都能听懂的方式表达."

agent = create_agent(
    model=llm,
    tools=tools,
    middleware=[user_level_prompt],
    context_schema={"level": str}
)
python

分别以 "expert""beginner" 调用:

result = agent.invoke(
    {"messages": [{"role": "user", "content": "解释一下朴素贝叶斯算法"}]},
    context={"level": "expert"}
) 
result = agent.invoke(
    {"messages": [{"role": "user", "content": "解释一下朴素贝叶斯算法"}]},
    context={"level": "beginner"}
) 
python

预期差异

  • 专家版:包含公式、术语、推导过程;
  • 初学者版:使用比喻、生活例子、避免数学。

[专家版输出]

现在的用户level:expert
{'messages': [HumanMessage(content='解释一下朴素贝叶斯算法', additional_kwargs={}, response_metadata={}, id='8bacbc64-aad3-46dd-a46d-05c91f906a3e'), AIMessage(content='朴素贝叶斯算法是一种基于贝叶斯定理并假设特征之间相互独立的概率分类方法。它广泛应用于文本分类、垃圾邮件过滤、情感分析等自然语言处理领域。\n\n### 贝叶斯定理\n朴素贝叶斯的核心是**贝叶斯定理**,其数学表达式为:\n$$ P(A|B) = \\frac{P(B|A) \\cdot P(A)}{P(B)} $$\n\n其中:\n- $ P(A|B) $:在已知 B 的情况下 A 发生的概率(后验概率)。\n- $ P(B|A) $:在已知 A 的情况下 B 发生的概率(似然度)。\n- $ P(A) $:A 发生的先验概率。\n- $ P(B) $:B 发生的先验概率。\n\n### 朴素贝叶斯的“朴素”假设\n朴素贝叶斯的关键假设是**特征之间的条件独立性**。也就是说,在给定类别的情况下,所有特征彼此独立。这使得计算更加简单,并且在许多实际应用中表现良好。\n\n### 算法流程\n1. **数据准备**:\n   - 将数据集划分为训练集和测试集。\n   - 对于文本分类问题,通常需要对文本进行分词、去停用词、提取特征等预处理操作。\n\n2. **训练模型**:\n   - 统计每个类别下各个特征出现的频率。\n   - 计算先验概率 $ P(C_k) $,即每个类别的概率。\n   - 计算条件概率 $ P(x_i | C_k) $,即在某个类别下某个特征出现的概率。\n\n3. **预测新样本**:\n   - 对于一个新的样本,计算其属于每个类别的后验概率 $ P(C_k | x_1, x_2, ..., x_n) $。\n   - 根据最大后验概率原则,选择概率最大的类别作为预测结果。\n\n### 常见变种\n朴素贝叶斯有几种常见的变种,适用于不同类型的数据:\n\n1. **多项式朴素贝叶斯(Multinomial Naive Bayes)**:\n   - 适用于离散特征,尤其是文本分类中的词频统计。\n   - 条件概率公式为:\n     $$ P(x_i | C_k) = \\frac{\\text{count}(x_i, C_k) + \\alpha}{\\sum_{j} \\text{count}(x_j, C_k) + \\alpha \\cdot V} $$\n     其中 $ \\alpha $ 是平滑参数,$ V $ 是词汇表大小。\n\n2. **伯努利朴素贝叶斯(Bernoulli Naive Bayes)**:\n   - 适用于二值特征(0 或 1),例如是否包含某个单词。\n   - 条件概率公式为:\n     $$ P(x_i | C_k) = \\begin{cases} \n     p_{i,k} & \\text{if } x_i = 1 \\\\\n     1 - p_{i,k} & \\text{if } x_i = 0 \n     \\end{cases} $$\n     其中 $ p_{i,k} $ 是在类别 $ C_k $ 下特征 $ x_i $ 出现的概率。\n\n3. **高斯朴素贝叶斯(Gaussian Naive Bayes)**:\n   - 适用于连续特征,假设特征服从正态分布。\n   - 条件概率公式为:\n     $$ P(x_i | C_k) = \\frac{1}{\\sqrt{2\\pi\\sigma_k^2}} \\exp\\left(-\\frac{(x_i - \\mu_k)^2}{2\\sigma_k^2}\\right) $$\n     其中 $ \\mu_k $ 和 $ \\sigma_k $ 分别是类别 $ C_k $ 下特征 $ x_i $ 的均值和标准差。\n\n### 优缺点\n#### 优点:\n1. **简单高效**:计算复杂度低,适合大规模数据集。\n2. **对小规模数据有效**:即使在小样本情况下也能取得较好的效果。\n3. **对缺失数据不敏感**:能够处理部分缺失的数据。\n\n#### 缺点:\n1. **特征独立性假设过于理想化**:现实世界中特征之间可能存在相关性,这会影响模型的准确性。\n2. **对输入数据的分布敏感**:如果特征不服从假设的分布(如高斯分布),可能会影响模型性能。\n\n### 应用场景\n朴素贝叶斯算法因其简单性和高效性,常用于以下场景:\n- **文本分类**:如新闻分类、垃圾邮件检测、情感分析等。\n- **推荐系统**:根据用户的历史行为预测兴趣。\n- **医学诊断**:根据患者的症状预测疾病类型。\n\n### 总结\n朴素贝叶斯是一种简单但强大的分类算法,尤其适合处理文本数据。虽然它的“朴素”假设在某些情况下可能不太合理,但在实践中往往能取得很好的效果。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 1064, 'prompt_tokens': 495, 'total_tokens': 1559, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'Qwen3-32B', 'system_fingerprint': None, 'id': 'chatcmpl-00ee2b2f-0021-4fb8-be16-ec56012b26f2', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--25c8b7bd-2203-4a58-9a9d-d5e71990ddb1-0', usage_metadata={'input_tokens': 495, 'output_tokens': 1064, 'total_tokens': 1559, 'input_token_details': {}, 'output_token_details': {}})]}
bash

[初学者版输出]

现在的用户level:beginner
{'messages': [HumanMessage(content='解释一下朴素贝叶斯算法', additional_kwargs={}, response_metadata={}, id='609572f6-3404-47f7-8d67-d09ed38c2440'), AIMessage(content='朴素贝叶斯算法是一种帮助我们做决定的聪明方法。我们可以把它想象成一个猜谜游戏,这个游戏根据我们看到的东西来猜出答案。\n\n比如说,如果我们想要猜一封邮件是不是垃圾邮件,朴素贝叶斯就会看看这封邮件里有哪些词。如果它看到很多像“免费”、“赢取大奖”这样的词,它可能会猜这是一封垃圾邮件。但如果它看到的是“会议”、“报告”这样的词,它可能猜这不是垃圾邮件。\n\n这个算法之所以叫“朴素”,是因为它假设每个词都是独立的,也就是说,它不考虑这些词之间的关系,只是单独地看每一个词。虽然这是一个简单的假设,但它在很多情况下都工作得很好!\n\n所以,总结一下,朴素贝叶斯就是通过观察一些特征(比如邮件里的词),然后根据这些特征出现的概率来做预测的一种方法。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 179, 'prompt_tokens': 499, 'total_tokens': 678, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'Qwen3-32B', 'system_fingerprint': None, 'id': 'chatcmpl-e308bde6-f71c-47ec-b588-54f0e917452a', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--2f33fe73-92e6-4429-bdb0-df695eef9e6b-0', usage_metadata={'input_tokens': 499, 'output_tokens': 179, 'total_tokens': 678, 'input_token_details': {}, 'output_token_details': {}})]}
bash

5. 结构化输出:强制返回 JSON#

当需要从文本中提取结构化信息时,可指定 response_format

class Resume(TypedDict):
    name: str
    age: str
    phone: str

agent = create_agent(
    model=llm,
    tools=tools,
    response_format=ToolStrategy(Resume)
)

result = agent.invoke({
    "messages": [{"role": "user", "content": "我叫宋昌,今年35岁,电话13812345678"}]
})
print(result["structured_response"])
python

结果
直接获得标准 JSON 对象,无需后处理。

{'messages': [HumanMessage(content='从以下信息中提取Resume信息。我叫宋昌,今年35岁,我的电话号码是13812345678', additional_kwargs={}, response_metadata={}, id='2f94d017-40ee-452d-915e-45defed616e0'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 42, 'prompt_tokens': 576, 'total_tokens': 618, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'Qwen3-32B', 'system_fingerprint': None, 'id': 'chatcmpl-02d310af-c4db-4046-871b-3fe5c7a46e0f', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--fd0db0ef-6c3b-4211-b6e8-746a25ef7c21-0', tool_calls=[{'name': 'Resume', 'args': {'age': '35', 'name': '宋昌', 'phone': '13812345678'}, 'id': 'call_f850f162d81f41e08875bf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 576, 'output_tokens': 42, 'total_tokens': 618, 'input_token_details': {}, 'output_token_details': {}}), ToolMessage(content="Returning structured response: {'name': '宋昌', 'age': '35', 'phone': '13812345678'}", name='Resume', id='4e5ad143-157b-46a3-a157-0424f87eb4ee', tool_call_id='call_f850f162d81f41e08875bf')], 'structured_response': {'name': '宋昌', 'age': '35', 'phone': '13812345678'}}
{'name': '宋昌', 'age': '35', 'phone': '13812345678'}
bash

6. 扩展 Agent 记忆:保存自定义状态#

Agent 默认记住对话历史(messages),但我们还可以添加自定义字段,如 user_info

方法一:通过中间件(适合需要干预流程的场景)#

class CustomState(AgentState):
    user_info: dict

class CustomMiddleware(AgentMiddleware):
    state_schema = CustomState
    def before_model(self, state, runtime):
        if state["user_info"]["title"] == "大将军":
            return {"messages": [SystemMessage(content="将军好!……"), *state["messages"]]}

agent = create_agent(llm, tools=tools, middleware=[CustomMiddleware()])

result = agent.invoke({
    "messages": [{"role": "user", "content": "我喜欢研究理论"}],
    "user_info": {"name": "赵无恤", "title": "大将军"}
})
python

输出如下:

当前状态: {'messages': [HumanMessage(content='我喜欢研究理论', additional_kwargs={}, response_metadata={}, id='3a726091-e837-474a-afe6-14e69e6829e7')], 'user_info': {'name': '赵无恤', 'title': '大将军'}}
【系统提示词】: 你正在与一位军事统帅对话,请使用庄重语气,并在交流前加上“将军好!”的表达。
{'messages': [HumanMessage(content='我喜欢研究理论', additional_kwargs={}, response_metadata={}, id='3a726091-e837-474a-afe6-14e69e6829e7'), SystemMessage(content='你正在与一位军事统帅对话,请使用庄重语气,并在交流前加上“将军好!”的表达。', additional_kwargs={}, response_metadata={}, id='c2c05fb1-d1ec-4861-9b04-ab856d8898dc'), AIMessage(content='将军好!理论研究是智慧的源泉,也是制定战略与决策的重要基础。您对哪一方面的理论感兴趣?无论是兵法、政治、哲学还是其他领域,我都很乐意为您提供相关信息或进行深入探讨。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 47, 'prompt_tokens': 500, 'total_tokens': 547, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'Qwen3-32B', 'system_fingerprint': None, 'id': 'chatcmpl-732de714-22e5-4a38-be2c-f73205b148ac', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--2b6b0aff-5d33-496f-81f1-5d3f5f5a4ee3-0', usage_metadata={'input_tokens': 500, 'output_tokens': 47, 'total_tokens': 547, 'input_token_details': {}, 'output_token_details': {}})], 'user_info': {'name': '赵无恤', 'title': '大将军'}}
bash

方法二:通过 state_schema(简洁,仅扩展数据)#

agent = create_agent(model=llm, tools=tools, state_schema=CustomState)

# 第一轮
state1= agent.invoke({
    "messages": [{"role": "user", "content": "我喜欢研究军事理论"}],
    "user_info": {"name": "赵无恤", "title": "大将军"},
})
python

输出如下:

{'messages': [HumanMessage(content='我喜欢研究军事理论', additional_kwargs={}, response_metadata={}, id='2044bd7c-e1e4-4bc4-8440-8c1ad9a7690c'), AIMessage(content='那非常有趣!军事理论涉及战略、战术、武器系统、军队组织等多个方面。你对哪个具体领域感兴趣呢?比如现代战争中的信息战、网络战,还是传统的陆海空三军作战理论?我可以为你提供一些相关的知识或资料。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 56, 'prompt_tokens': 475, 'total_tokens': 531, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'Qwen3-32B', 'system_fingerprint': None, 'id': 'chatcmpl-9d7ea05a-5378-4703-9c33-5c2eab5e8374', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--5380d835-ad4f-4812-8b63-d2b2b10d1c84-0', usage_metadata={'input_tokens': 475, 'output_tokens': 56, 'total_tokens': 531, 'input_token_details': {}, 'output_token_details': {}})], 'user_info': {'name': '赵无恤', 'title': '大将军'}}
bash
# 第二轮:传递完整 state 实现记忆延续
state2 = agent.invoke({
    **state1,
    "messages": state1["messages"] + [{"role": "user", "content": "推荐几本经典著作?"}]
})
python

输出如下:

✅ 这样,Agent 能够记得对话,实现真正个性化服务。


总结#

本文通过六个递进式实验,系统性地展示了 LangChain/LangGraph 框架下构建高阶 Agent 的关键技术路径。以下从技术架构层面进行归纳:

能力维度技术实现机制核心价值典型应用场景
工具集成与推理链编排通过 @tool 装饰器注册函数或类工具,结合 ReAct 或 Plan-and-Execute 等策略,由 LLM 动态决定工具调用顺序与参数。Agent 在单次推理中可完成“检索 → 计算 → 总结”的多步协同。实现 LLM 与外部能力(API、数据库、计算函数)的语义桥接,突破纯文本生成的局限。多跳问答、数据驱动决策、自动化报告生成等需组合多种能力的任务。
工具执行异常的中间件拦截利用 wrap_tool_call 中间件对工具调用进行包装,在 handler(request) 执行前后插入异常捕获逻辑,并返回符合 ToolMessage 协议的结构化错误响应。将底层运行时异常转化为 Agent 可理解、可恢复的语义信号,避免流程中断,提升鲁棒性。生产环境中对不可靠工具(如第三方 API、用户自定义函数)的安全封装。
上下文感知的动态模型路由通过自定义 Context 类携带元信息(如 provider),配合 wrap_model_call 中间件在请求分发前动态替换 request.model 字段,实现运行时模型切换。解耦 Agent 逻辑与具体 LLM 实现,支持按任务类型、成本、性能或合规要求动态调度异构模型。多租户 SaaS 应用、混合模型部署(如 Qwen 用于通用对话,DeepSeek 用于代码生成)。
个性化系统提示(Dynamic Prompting)基于运行时上下文(如用户角色 level)动态生成 system prompt,通过 @dynamic_prompt 中间件注入到模型输入前缀中,引导 LLM 输出风格与深度。实现细粒度的输出控制,无需训练多个专用模型即可适配不同用户认知水平或业务语境。教育产品(初学者/专家模式)、客服系统(普通用户/管理员视图)、医疗问答(患者/医生视角)。
强约束结构化输出(Structured Output)利用 response_format=ToolStrategy(schema) 指令,强制 LLM 以工具调用形式返回符合 Pydantic/TypedDict Schema 的 JSON 对象,绕过自由文本解析。消除后处理环节的不确定性,确保输出可直接用于下游系统(如数据库写入、API 响应),提升端到端可靠性。信息抽取(简历、票据、日志)、表单填充、API 参数生成等需高精度结构化结果的场景。
可扩展的状态管理(Custom State Schema)通过继承 AgentState 定义包含 user_info 等字段的自定义状态类型,并在 state_schema 中声明;中间件或后续步骤可读写该状态,实现跨轮次上下文持久化。超越仅维护消息历史的限制,支持业务相关上下文(如用户身份、会话偏好、任务进度)的端到端传递与更新。多轮复杂对话(如订票、诊断)、个性化推荐、带状态的工作流自动化。

架构启示:上述能力均基于 LangGraph 提供的可组合中间件(Middleware)机制显式状态图(State Graph)抽象。这种设计将传统 Agent 的“黑盒推理”转化为可观测、可干预、可扩展的执行流水线,为构建企业级可靠 AI 应用奠定了工程基础。

Agent实战(二)构建智能体的完整实践指南:工具调用、错误处理、动态模型选择与记忆扩展
http://www.soupcola.top/blog/agent_blogs/agent_blogs-2
Author Soup Cola
Published at 2026年2月12日