差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 两侧同时换到之前的修订记录 前一修订版 | |||
| 智能体二次开发:langchain:核心组件详解_tools [2026/05/20 19:02] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | 智能体二次开发:langchain:核心组件详解_tools [2026/05/20 19:02] (当前版本) – ↷ 页面langchain二次开发:核心组件详解_tools被移动至智能体二次开发:langchain:核心组件详解_tools 张叶安 | ||
|---|---|---|---|
| 行 1: | 行 1: | ||
| + | ====== 第六章:核心组件详解 - Tools ====== | ||
| + | |||
| + | 如果说模型负责“理解、推理和表达”,那么工具(Tools)负责“接触真实世界”。没有工具的模型,只能基于训练数据和当前上下文进行回答;有了工具,模型才能查天气、查订单、读数据库、检索知识库、发送草稿、访问内部接口。因此,工具是从“会聊天”走向“能办事”的关键桥梁。 | ||
| + | |||
| + | 本章会从 Tool 的概念、定义方式、Schema 设计、返回值设计、状态访问、错误处理,到与 Agent 协同工作的方法,系统讲清楚如何构建真正“好用”的工具。 | ||
| + | |||
| + | ===== 6.1 什么是 Tool ===== | ||
| + | |||
| + | ==== 6.1.1 Tool 的本质 ==== | ||
| + | |||
| + | Tool 本质上是一段可被模型调用的函数能力。根据 LangChain 官方文档,一个工具通常包含三个关键元素: | ||
| + | * 名称:模型看到的工具名; | ||
| + | * 描述:模型判断“何时该用”的依据; | ||
| + | * 参数 Schema:模型知道“怎么调用”的结构。 | ||
| + | |||
| + | 最容易理解的比喻是:**Tool 就是给模型看的 API 文档。** | ||
| + | |||
| + | 如果你只给程序员看,它是函数;如果你既要给程序员看,也要给模型看,它就必须设计成 Tool。 | ||
| + | |||
| + | ==== 6.1.2 为什么普通函数不够 ==== | ||
| + | |||
| + | 普通函数通常只考虑: | ||
| + | * 逻辑是否正确; | ||
| + | * 参数是否完整; | ||
| + | * 返回是否可用。 | ||
| + | |||
| + | 而 Tool 还要额外考虑: | ||
| + | * 模型能不能理解这个工具是干什么的; | ||
| + | * 模型会不会误用它; | ||
| + | * 模型能不能正确填参数; | ||
| + | * 返回结果是否适合下一步推理。 | ||
| + | |||
| + | ===== 6.2 使用 `@tool` 定义工具 ===== | ||
| + | |||
| + | ==== 6.2.1 最小工具示例 ==== | ||
| + | |||
| + | <code python> | ||
| + | from langchain_core.tools import tool | ||
| + | |||
| + | @tool | ||
| + | def get_weather(city: | ||
| + | """ | ||
| + | return f" | ||
| + | </ | ||
| + | |||
| + | 这个工具已经具备: | ||
| + | * 工具名:`get_weather` | ||
| + | * 描述:来自 docstring | ||
| + | * 参数:`city: | ||
| + | |||
| + | ==== 6.2.2 为什么 docstring 很重要 ==== | ||
| + | |||
| + | LangChain 官方文档明确强调,工具 docstring 应当简洁且信息充分,因为它直接帮助模型理解何时使用该工具。 | ||
| + | |||
| + | 一个好的 docstring 应该回答: | ||
| + | * 什么时候该用; | ||
| + | * 参数分别表示什么; | ||
| + | * 结果大致返回什么; | ||
| + | * 是否有边界限制。 | ||
| + | |||
| + | 坏例子: | ||
| + | * “查询接口。” | ||
| + | |||
| + | 好例子: | ||
| + | * “根据订单编号查询订单状态和物流信息,仅适用于已创建订单。” | ||
| + | |||
| + | ===== 6.3 参数 Schema 设计 ===== | ||
| + | |||
| + | ==== 6.3.1 简单参数 ==== | ||
| + | |||
| + | 如果参数很少,直接靠函数签名即可。 | ||
| + | |||
| + | <code python> | ||
| + | @tool | ||
| + | def exchange_rate(base_currency: | ||
| + | """ | ||
| + | return f"1 {base_currency} = 7.21 {quote_currency}" | ||
| + | </ | ||
| + | |||
| + | ==== 6.3.2 使用 Pydantic 做复杂参数约束 ==== | ||
| + | |||
| + | 复杂工具更推荐使用 Pydantic 定义 Schema,这样字段描述会更清晰。 | ||
| + | |||
| + | <code python> | ||
| + | from pydantic import BaseModel, Field | ||
| + | from typing import Literal | ||
| + | from langchain_core.tools import tool | ||
| + | |||
| + | class WeatherInput(BaseModel): | ||
| + | city: str = Field(description=" | ||
| + | units: Literal[" | ||
| + | default=" | ||
| + | description=" | ||
| + | ) | ||
| + | include_forecast: | ||
| + | default=False, | ||
| + | description=" | ||
| + | ) | ||
| + | |||
| + | @tool(args_schema=WeatherInput) | ||
| + | def get_weather(city: | ||
| + | """ | ||
| + | temp = 26 if units == " | ||
| + | result = f" | ||
| + | if include_forecast: | ||
| + | result += " | ||
| + | return result | ||
| + | </ | ||
| + | |||
| + | ==== 6.3.3 设计参数时的实践建议 ==== | ||
| + | |||
| + | * 字段名应清晰,不要用 `x1`、`arg2` 这类无语义名字; | ||
| + | * 能枚举就枚举,例如 `priority` 用 `low/ | ||
| + | * 描述要体现业务语义,不只是技术含义; | ||
| + | * 尽量避免一个工具接受十几个松散参数; | ||
| + | * 高风险参数要明确写明取值规则。 | ||
| + | |||
| + | ===== 6.4 工具返回什么最合适 ===== | ||
| + | |||
| + | 根据 LangChain 官方文档,工具返回值可以是: | ||
| + | * 字符串; | ||
| + | * 对象 / dict; | ||
| + | * `Command`(用于更新状态)。 | ||
| + | |||
| + | ==== 6.4.1 返回字符串 ==== | ||
| + | |||
| + | 适合模型直接阅读的自然语言结果。 | ||
| + | |||
| + | <code python> | ||
| + | @tool | ||
| + | def get_inventory(sku: | ||
| + | """ | ||
| + | return f"SKU {sku} 当前库存 23 件,位于华东一号仓。" | ||
| + | </ | ||
| + | |||
| + | 适合场景: | ||
| + | * 返回结果本身就是一句清晰结论; | ||
| + | * 模型只需继续基于文字推理; | ||
| + | * 结果结构简单。 | ||
| + | |||
| + | ==== 6.4.2 返回结构化对象 ==== | ||
| + | |||
| + | 如果你希望模型能显式读取字段,返回 `dict` 更合适。 | ||
| + | |||
| + | <code python> | ||
| + | @tool | ||
| + | def get_inventory_data(sku: | ||
| + | """ | ||
| + | return { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 适合场景: | ||
| + | * 下游模型需要基于多个字段推理; | ||
| + | * 你希望结果更稳定可解析; | ||
| + | * 后面可能会把同一个工具复用于 API 或审计系统。 | ||
| + | |||
| + | ==== 6.4.3 返回 Command 更新状态 ==== | ||
| + | |||
| + | 在 LangChain / LangGraph 体系中,某些工具不仅返回数据,还要直接修改 Agent 状态。这时可以返回 `Command`。 | ||
| + | |||
| + | <code python> | ||
| + | from langchain_core.tools import ToolRuntime, | ||
| + | from langchain_core.messages import ToolMessage | ||
| + | from langgraph.types import Command | ||
| + | |||
| + | @tool | ||
| + | def set_language(language: | ||
| + | """ | ||
| + | return Command( | ||
| + | update={ | ||
| + | " | ||
| + | " | ||
| + | ToolMessage( | ||
| + | content=f" | ||
| + | tool_call_id=runtime.tool_call_id, | ||
| + | ) | ||
| + | ], | ||
| + | } | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | 这类工具适合: | ||
| + | * 保存用户偏好; | ||
| + | * 更新当前任务状态; | ||
| + | * 写入工作流共享字段。 | ||
| + | |||
| + | ===== 6.5 在工具中访问运行时状态 ===== | ||
| + | |||
| + | LangChain 官方文档提到,可以通过 `runtime: ToolRuntime` 访问会话状态,这个参数不会暴露给模型,只会在运行时自动注入。 | ||
| + | |||
| + | ==== 6.5.1 读取最近一条用户消息 ==== | ||
| + | |||
| + | <code python> | ||
| + | from langchain_core.tools import tool, ToolRuntime | ||
| + | from langchain_core.messages import HumanMessage | ||
| + | |||
| + | @tool | ||
| + | def get_last_user_message(runtime: | ||
| + | """ | ||
| + | messages = runtime.state[" | ||
| + | for message in reversed(messages): | ||
| + | if isinstance(message, | ||
| + | return message.content | ||
| + | return " | ||
| + | </ | ||
| + | |||
| + | ==== 6.5.2 读取自定义状态 ==== | ||
| + | |||
| + | <code python> | ||
| + | @tool | ||
| + | def get_user_preference(pref_name: | ||
| + | """ | ||
| + | preferences = runtime.state.get(" | ||
| + | return str(preferences.get(pref_name, | ||
| + | </ | ||
| + | |||
| + | 这种方式的好处是: | ||
| + | * 工具可以更“上下文化”; | ||
| + | * 不必要求模型把所有状态字段都手工重复传参; | ||
| + | * 对复杂 Agent 特别有价值。 | ||
| + | |||
| + | ===== 6.6 把工具绑定给模型 ===== | ||
| + | |||
| + | ==== 6.6.1 使用 `bind_tools()` ==== | ||
| + | |||
| + | <code python> | ||
| + | from langchain_openai import ChatOpenAI | ||
| + | |||
| + | model = ChatOpenAI(model=" | ||
| + | model_with_tools = model.bind_tools([get_weather, | ||
| + | response = model_with_tools.invoke(" | ||
| + | print(response.tool_calls) | ||
| + | </ | ||
| + | |||
| + | 绑定之后,模型就具备了“选择工具”的能力。但你仍需要决定: | ||
| + | * 是手动执行工具调用闭环; | ||
| + | * 还是交给 Agent 框架自动完成。 | ||
| + | |||
| + | ==== 6.6.2 手动执行工具调用 ==== | ||
| + | |||
| + | <code python> | ||
| + | response = model_with_tools.invoke(" | ||
| + | for tool_call in response.tool_calls: | ||
| + | print(" | ||
| + | print(" | ||
| + | </ | ||
| + | |||
| + | 这个阶段很适合调试: | ||
| + | * 模型到底会不会选对工具; | ||
| + | * 参数有没有填错; | ||
| + | * 工具描述是否足够清楚。 | ||
| + | |||
| + | ===== 6.7 在 Agent 中使用工具 ===== | ||
| + | |||
| + | 大多数真实项目不会手写每一轮工具循环,而是直接交给 `create_agent()`。 | ||
| + | |||
| + | <code python> | ||
| + | from langchain.agents import create_agent | ||
| + | |||
| + | agent = create_agent( | ||
| + | model=" | ||
| + | tools=[get_weather, | ||
| + | system_prompt=" | ||
| + | ) | ||
| + | |||
| + | result = agent.invoke({ | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | print(result) | ||
| + | </ | ||
| + | |||
| + | 此时 Agent 会: | ||
| + | * 读取用户消息; | ||
| + | * 判断是否要调工具; | ||
| + | * 执行工具; | ||
| + | * 将 ToolMessage 回传; | ||
| + | * 再次调用模型汇总答案。 | ||
| + | |||
| + | ===== 6.8 工具错误处理 ===== | ||
| + | |||
| + | 工具连接的是外部世界,所以错误不是异常情况,而是常态: | ||
| + | * 网络超时; | ||
| + | * 参数错误; | ||
| + | * 权限不足; | ||
| + | * 数据为空; | ||
| + | * 下游返回格式变化; | ||
| + | * 第三方接口不可用。 | ||
| + | |||
| + | ==== 6.8.1 在工具内部做基础兜底 ==== | ||
| + | |||
| + | <code python> | ||
| + | @tool | ||
| + | def safe_order_query(order_id: | ||
| + | """ | ||
| + | try: | ||
| + | if not order_id.startswith(" | ||
| + | return " | ||
| + | return f" | ||
| + | except TimeoutError: | ||
| + | return " | ||
| + | except Exception as exc: | ||
| + | return f" | ||
| + | </ | ||
| + | |||
| + | ==== 6.8.2 使用中间件统一监控工具调用 ==== | ||
| + | |||
| + | LangChain 官方自定义中间件文档提供了 `@wrap_tool_call` 的模式,适合做日志、监控、统一错误包装。 | ||
| + | |||
| + | <code python> | ||
| + | from collections.abc import Callable | ||
| + | from langchain.agents.middleware import wrap_tool_call | ||
| + | from langchain.messages import ToolMessage | ||
| + | from langchain.tools.tool_node import ToolCallRequest | ||
| + | from langgraph.types import Command | ||
| + | |||
| + | @wrap_tool_call | ||
| + | def monitor_tool( | ||
| + | request: ToolCallRequest, | ||
| + | handler: Callable[[ToolCallRequest], | ||
| + | ) -> ToolMessage | Command: | ||
| + | print(f" | ||
| + | print(f" | ||
| + | try: | ||
| + | result = handler(request) | ||
| + | print(" | ||
| + | return result | ||
| + | except Exception as e: | ||
| + | print(f" | ||
| + | raise | ||
| + | </ | ||
| + | |||
| + | 这类中间件适合做: | ||
| + | * 调用日志; | ||
| + | * 指标采集; | ||
| + | * 错误监控; | ||
| + | * 风险操作审计。 | ||
| + | |||
| + | ===== 6.9 工具设计的工程原则 ===== | ||
| + | |||
| + | ==== 6.9.1 一个工具只做一件事 ==== | ||
| + | |||
| + | 坏工具: | ||
| + | * `business_assistant()`:既查天气、又查订单、又发邮件。 | ||
| + | |||
| + | 好工具: | ||
| + | * `get_weather()` | ||
| + | * `query_order_status()` | ||
| + | * `draft_email()` | ||
| + | * `create_approval_draft()` | ||
| + | |||
| + | 拆分的好处: | ||
| + | * 便于模型选择; | ||
| + | * 便于测试; | ||
| + | * 便于权限控制; | ||
| + | * 便于指标分析。 | ||
| + | |||
| + | ==== 6.9.2 工具描述要写“使用时机” ==== | ||
| + | |||
| + | 模型关心的不是你的内部接口路径,而是“我什么时候该调用这个工具”。 | ||
| + | |||
| + | 例如: | ||
| + | * 好描述:根据订单号查询订单当前状态和物流信息,仅适用于已创建订单。 | ||
| + | * 坏描述:调用 `/ | ||
| + | |||
| + | ==== 6.9.3 输出要面向模型推理 ==== | ||
| + | |||
| + | 不要机械返回原始 JSON。更好的方式通常是: | ||
| + | * 过滤无关字段; | ||
| + | * 统一单位和时间格式; | ||
| + | * 标明数据更新时间; | ||
| + | * 在必要时补充解释; | ||
| + | * 对异常场景返回模型可理解的提示。 | ||
| + | |||
| + | ===== 6.10 高风险工具的安全边界 ===== | ||
| + | |||
| + | 不是所有工具都能直接暴露给 Agent。尤其是: | ||
| + | * 写数据库; | ||
| + | * 发正式邮件; | ||
| + | * 退款; | ||
| + | * 创建审批单; | ||
| + | * 删除数据; | ||
| + | * 执行系统命令。 | ||
| + | |||
| + | 这类工具建议采取分层策略: | ||
| + | * **只读工具**:可直接开放; | ||
| + | * **草拟工具**:允许生成草稿,不允许最终提交; | ||
| + | * **审批工具**:必须人工确认后执行; | ||
| + | * **禁止暴露工具**:只允许后台系统调用。 | ||
| + | |||
| + | ==== 6.10.1 邮件工具双阶段设计 ==== | ||
| + | |||
| + | <code python> | ||
| + | @tool | ||
| + | def draft_email(to: | ||
| + | """ | ||
| + | return { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 真正发送邮件应由: | ||
| + | * 用户在界面确认; | ||
| + | * 或工作流走到人工审批节点后; | ||
| + | * 再调用后端受控接口完成。 | ||
| + | |||
| + | ===== 6.11 一个完整的多工具案例 ===== | ||
| + | |||
| + | 下面给出一个更接近真实业务的例子: | ||
| + | |||
| + | <code python> | ||
| + | from langchain.agents import create_agent | ||
| + | from langchain_core.tools import tool | ||
| + | |||
| + | @tool | ||
| + | def query_order(order_id: | ||
| + | """ | ||
| + | return { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | @tool | ||
| + | def query_inventory(sku: | ||
| + | """ | ||
| + | return { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | @tool | ||
| + | def draft_delay_notice(order_id: | ||
| + | """ | ||
| + | return f" | ||
| + | |||
| + | agent = create_agent( | ||
| + | model=" | ||
| + | tools=[query_order, | ||
| + | system_prompt=""" | ||
| + | 你是订单运营助手。 | ||
| + | 1. 涉及订单、库存等事实信息时,必须优先调用工具。 | ||
| + | 2. 不要编造库存数字或发货状态。 | ||
| + | 3. 涉及通知用户时,只生成草稿,不得声称已经发送。 | ||
| + | """ | ||
| + | ) | ||
| + | |||
| + | result = agent.invoke({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }] | ||
| + | }) | ||
| + | |||
| + | print(result) | ||
| + | </ | ||
| + | |||
| + | ===== 6.12 本章小结 ===== | ||
| + | |||
| + | * Tool 是模型接触真实世界的接口层; | ||
| + | * 好工具不仅要功能正确,还要让模型容易理解和正确使用; | ||
| + | * 参数 Schema、描述和返回格式会直接影响工具调用质量; | ||
| + | * `ToolRuntime` 和 `Command` 让工具具备读取和更新状态的能力; | ||
| + | * 高风险工具必须加人工确认或工作流控制; | ||
| + | * 统一监控和错误处理,是工具工程化的关键。 | ||
| + | |||
| + | ===== 练习 ===== | ||
| + | |||
| + | 1. 实现 3 个工具:天气查询、订单查询、库存查询,并为每个工具编写清晰的 docstring 与参数 Schema。 | ||
| + | |||
| + | 2. 设计一个“邮件发送”双阶段流程:Agent 只能生成草稿,用户确认后才能真正发送。 | ||
| + | |||
| + | 3. 将一个原始 JSON 接口返回值,重构为更适合模型理解的文本或结构化对象输出。 | ||
| + | |||
| + | 4. 使用 `@wrap_tool_call` 编写一个工具监控中间件,记录工具名、参数和执行时长。 | ||
| + | |||
| + | 5. 为你的业务设计一个不超过 8 个工具的 Agent 工具集,并说明每个工具的边界。 | ||
| + | |||
| + | ===== 参考资源 ===== | ||
| + | |||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | |||