LangChain 03:工具与 Agent——从函数到可控行动
1、本篇任务:让 Agent 安全地读取订单
上一课得到结构化分诊结果,但模型不知道真实订单。本课增加一个只读工具 get_order,走通完整 Agent 循环,并把模型、工具包装和领域服务的责任分开。
完成后,用户问“我的 A100 发货了吗”,Agent 会选择工具;工具只按当前登录用户查询;模型基于工具结果回答。它仍然不能退款,因为我们没有提供写工具。
2、工具的本质是受 schema 约束的应用函数
一个好工具需要:稳定名称、清楚描述、窄输入 schema、结构化结果、服务端权限、受控错误。描述写给模型看,权限写给服务端执行;两者不能互相替代。
模型决定:是否调用 get_order、传哪个 order_id
工具层决定:参数是否合法、当前用户是谁、能否查询
领域服务决定:订单是否存在、属于谁、返回哪些字段
工具参数中不要包含 user_id 或 tenant_id 让模型填写。可信身份来自认证后的 runtime/context。
3、先写与 LangChain 无关的领域服务
Python:
from dataclasses import dataclass
@dataclass(frozen=True)
class Actor:
user_id: str
tenant_id: str
class OrderService:
def get_for_actor(self, actor: Actor, order_id: str) -> dict:
row = DATABASE.get(order_id)
if row is None or row["tenant_id"] != actor.tenant_id or row["user_id"] != actor.user_id:
raise LookupError("ORDER_NOT_FOUND")
return {"order_id": row["id"], "status": row["status"], "delivered_at": row["delivered_at"]}
这段服务可用普通单元测试验证,不需要模型、不需要 LangChain。工具只是它的 Agent 适配器。
4、把领域服务包装成工具
Python 的工具可从 ToolRuntime 读取可信 context:
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
@dataclass
class RequestContext:
user_id: str
tenant_id: str
@tool
def get_order(order_id: str, runtime: ToolRuntime[RequestContext]) -> dict:
"""查询当前登录用户的一笔订单。只用于读取状态,不修改订单。"""
actor = Actor(runtime.context.user_id, runtime.context.tenant_id)
try:
return order_service.get_for_actor(actor, order_id)
except LookupError:
return {"ok": False, "code": "ORDER_NOT_FOUND"}
agent = create_agent(
model="openai:gpt-5.5",
tools=[get_order],
context_schema=RequestContext,
system_prompt="回答订单问题前必须调用 get_order;不要猜测。",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "A100 发货了吗?"}]},
context=RequestContext(user_id="u-1", tenant_id="t-1"),
)
TypeScript 中保持同一结构:Zod 只暴露 orderId,用户身份由服务端闭包或 runtime 注入。
import { createAgent, tool } from "langchain";
import * as z from "zod";
function toolsFor(actor: Actor) {
const getOrder = tool(
async ({ orderId }) => {
try { return await orderService.getForActor(actor, orderId); }
catch { return { ok: false, code: "ORDER_NOT_FOUND" }; }
},
{
name: "get_order",
description: "查询当前登录用户的一笔订单,只读",
schema: z.object({ orderId: z.string().min(1).max(64) }),
},
);
return [getOrder];
}
const agent = createAgent({
model: "openai:gpt-5.5",
tools: toolsFor(authenticatedActor),
systemPrompt: "回答订单问题前必须调用 get_order;不要猜测。",
});
闭包创建的工具不能被跨用户缓存,否则会把 A 用户身份带进 B 用户请求。更完整的应用优先使用框架 runtime context。
5、逐步读懂 Agent 循环
用户消息进入后,模型可能产生 get_order tool call;Harness 校验参数、执行函数,再把结果作为 Tool Message 追加到 state;模型第二次调用看到真实状态并给最终答复。
这意味着一次用户请求可能包含多次模型调用。必须设置工具调用次数、总 token、超时和递归上限。模型不是循环终止条件的唯一保障。
6、工具错误不要直接抛给用户
将错误分成三类:
| 类型 | 示例 | 返回策略 |
|---|---|---|
| 用户可修复 | 订单号格式错误 | 稳定错误码,提示重新输入 |
| 权限/不存在 | 其他用户订单 | 统一 ORDER_NOT_FOUND,避免枚举资源 |
| 系统故障 | 数据库超时 | 记录内部异常,给模型 TEMPORARILY_UNAVAILABLE |
不要把 SQL、堆栈、数据库地址作为 Tool Message 交给模型。Tool Message 会进入上下文,也可能进入 trace。
7、写工具为什么更难
如果以后添加 submit_refund,必须有后端权限、业务状态校验、幂等键、审计和人工审批。模型产生合法参数不代表用户已经授权。正确顺序是先创建退款草案,再中断等待批准,批准后由领域服务提交;第 05 篇实现这条链路。
8、本篇测试
- 领域服务:用户只能查询自己的订单。
- 工具 schema:空订单号、超长订单号被拒绝。
- 工具错误:不存在与越权均只返回
ORDER_NOT_FOUND。 - Agent 行为:订单问题会调用工具;闲聊不会调用;工具失败不编造状态。
- 预算:模型连续调用工具时达到上限并受控终止。
本课产物是安全的只读订单工具。下一课增加政策知识库,并让 Agent 同时组合数据库事实和文档证据。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录