📖 本章学习目标
完成本章后,你将能够:
- ✅ 理解
createAgent()内部的 LangGraph 图结构- ✅ 掌握 ReAct 推理模式的执行流程和边界条件
- ✅ 使用 LangGraph API 手动构建等效的 Agent
- ✅ 编写中间件拦截和修改 Agent 行为
- ✅ 实现 Human-in-the-loop 人工确认机制
- ✅ 判断何时需要使用底层 LangGraph API
一、揭开 createAgent() 的内部
在前几章,我们一直在用 createAgent() 这个高层 API。它简洁易用,但理解其内部机制,是从"能用"跨越到"用好"的关键。
1、createAgent() 的本质
createAgent() 本质上是在 LangGraph 之上构建了一个标准化的 Agent 图。
createAgent()快速方便,适合大多数场景- LangGraph:灵活可控,适合定制化需求
2、Agent 图的完整结构
flowchart TD
Start(["__start__<br/>图的入口"]) --> Agent
subgraph AgentLoop["Agent 执行循环"]
direction TB
Agent["agent 节点<br/>调用 LLM 做推理决策"] -->|"有工具调用"| Tools
Tools["tools 节点<br/>执行工具调用"] --> Agent
end
Agent -->|"无工具调用<br/>最终回答"| End(["__end__<br/>图的出口"])
style Start fill:#e8f4fd,stroke:#1890ff,stroke-width:3px
style End fill:#f6ffed,stroke:#52c41a,stroke-width:3px
style Agent fill:#fff7e6,stroke:#fa8c16,stroke-width:3px
style Tools fill:#fff0f6,stroke:#eb2f96,stroke-width:3px
关键组件说明:
| 组件 | 类型 | 作用 |
|---|---|---|
__start__ | 特殊节点 | 图的入口,接收用户输入 |
agent | 普通节点 | 调用 LLM,决定下一步行动 |
tools | 普通节点 | 执行工具调用 |
__end__ | 特殊节点 | 图的出口,返回最终结果 |
| 条件边 | 动态路由 | 根据 LLM 输出决定流向 |
3、数据流详解
让我们跟踪一次完整的 Agent 执行过程:
第 1 步:用户输入进入
const result = await agent.invoke({
messages: [{ role: "user", content: "北京今天天气如何?" }]
});
状态变化:
// 初始状态
{
messages: [HumanMessage("北京今天天气如何?")]
}
第 2 步:进入 agent 节点
async function agentNode(state) {
// 1. 将所有消息发送给 LLM
const response = await modelWithTools.invoke(state.messages);
// 2. 将 LLM 响应追加到消息列表
return { messages: [response] };
}
可能的 LLM 输出:
AIMessage({
content: "",
tool_calls: [{
name: "get_weather",
args: { city: "北京" }
}]
})
第 3 步:条件判断
function shouldContinue(state) {
const lastMessage = state.messages.at(-1);
if (lastMessage.tool_calls?.length) {
return "tools"; // 有工具调用,转到 tools 节点
}
return END; // 没有工具调用,结束
}
第 4 步:执行工具
// tools 节点自动执行所有工具调用
const toolResults = await executeTools(toolCalls);
// 返回:[{ name: "get_weather", result: "北京晴,22°C" }]
第 5 步:回到 agent 节点
LLM 基于工具结果再次推理,可能:
- 继续调用其他工具
- 给出最终回答
第 6 步:结束
当 LLM 不再调用工具时,流程结束。
二、ReAct 推理模式
ReAct 是目前最主流的 Agent 推理框架,源自 2022 年的同名论文。
1、什么是 ReAct?
ReAct = Reasoning(推理)+ Acting(行动)
核心思想:把推理和行动交替进行,每次行动的结果都成为下一轮推理的输入。
【辅助理解】想象你在解决一个复杂问题:
- 思考:我需要查什么信息?
- 行动:上网搜索
- 观察:看到搜索结果
- 再思考:还需要什么信息?
- 再行动:打开具体网页
- 观察:阅读详细内容
- 得出结论:综合所有信息回答问题
这就是 ReAct 的工作方式。
2、ReAct 的完整执行过程
让我们通过一个实际例子来理解:
用户请求: "帮我查一下 LangChain.js 的最新版本号,并告诉我有没有新的 API"
第 1 轮:初始推理
┌────────────────────────────────────────────┐
│ Thought(思考): │
│ 用户想知道 LangChain.js 的最新版本, │
│ 我需要搜索最新信息。 │
│ 应该先搜索 LangChain.js 的 changelog 或 │
│ npm 页面。 │
│ │
│ Action(行动): │
│ 调用 search(query="LangChain.js latest │
│ version 2025") │
└─────────────────────────────────────────────┘
第 1 次观察
┌─────────────────────────────────────────────┐
│ Observation(观察): │
│ 搜索结果:LangChain.js v1.3.1 于 │
│ 2025年12月发布... │
└─────────────────────────────────────────────┘
第 2 轮:基于观察的推理
┌─────────────────────────────────────────────┐
│ Thought(思考): │
│ 找到了版本号是 1.3.1。 │
│ 还需要了解新 API,应该进一步搜索变更日志。 │
│ │
│ Action(行动): │
│ 调用 fetch_webpage(url="https://docs. │
│ langchain.com/.../changelog") │
└─────────────────────────────────────────────┘
第 2 次观察
┌─────────────────────────────────────────────┐
│ Observation(观察): │
│ v1.3.0 新增 Anthropic 服务端工具、 │
│ MCP 协议适配器支持... │
└─────────────────────────────────────────────┘
最终回答
┌─────────────────────────────────────────────┐
│ Final Answer(最终答案): │
│ LangChain.js 最新版本是 v1.3.1。主要新增了: │
│ 1. Anthropic 服务端工具支持... │
│ 2. MCP 协议适配器... │
└─────────────────────────────────────────────┘
3、ReAct 的优势
| 优势 | 说明 | 对比 |
|---|---|---|
| 可解释性强 | 每一步推理都清晰可见 | vs 直接生成答案 |
| 准确性高 | 基于真实信息回答 | vs 依赖训练数据 |
| 灵活性好 | 可根据情况调整策略 | vs 固定流程 |
| 易于调试 | 可以追踪每步决策 | vs 黑盒模型 |
4、ReAct 的边界条件
ReAct 循环不会无限执行下去,有以下终止条件:
(1)配置最大迭代次数
const agent = createAgent({
model: "openai:gpt-4o",
tools: [searchTool],
// 配置最大迭代次数(防止死循环)
maxIterations: 10, // 默认值,可以根据需要调整
});
(2)终止条件对比
| 条件 | 触发时机 | 处理方式 | 示例 |
|---|---|---|---|
| 正常结束 | LLM 不再发出工具调用 | 返回最终回答 | "根据搜索结果..." |
| 达到最大迭代 | 超过 maxIterations | 强制结束,返回当前状态 | "已达到最大尝试次数" |
| 工具连续失败 | 可通过中间件自定义 | 抛出错误或降级处理 | "工具不可用,无法完成任务" |
(3)监控迭代次数
import { createMiddleware } from "langchain";
const iterationTracker = createMiddleware({
name: "IterationTracker",
beforeModel: async (request) => {
// 统计已执行的轮数
const toolMessages = request.messages.filter(
m => m.role === "tool"
);
console.log(`当前迭代次数:${toolMessages.length}`);
return request;
},
});
三、LangGraph 核心概念
理解 LangGraph 的核心抽象,帮助你在需要时下探到底层进行精细控制。
🔍 深入探讨: 完整的LangGraph教程请阅读专栏《深入浅出LangGraph》。
1、四大核心概念
mindmap
root((LangGraph 核心))
State 状态
定义图的数据结构
在节点间流动
Annotation 定义 Reducer
Node 节点
图的执行单元
接收状态
返回状态更新
Edge 边
普通边 固定路由
条件边 动态路由
连接节点
Graph 图
StateGraph 定义图结构
compile 编译为 Runnable
支持 checkpointer 持久化
2、概念详解
State(状态)
状态是图的数据结构,在节点间流动。
// 简单状态示例
const MyState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (current, next) => [...current, ...next],
default: () => [],
}),
counter: Annotation<number>({
reducer: (_, next) => next,
default: () => 0,
}),
});
关键点:
reducer:定义如何合并新旧状态default:提供初始值- 状态是不可变的,每次更新产生新状态
Node(节点)
节点是图的执行单元,接收状态,返回状态更新。
async function myNode(state: typeof MyState.State) {
// 读取当前状态
const currentCount = state.counter;
// 执行业务逻辑
const newCount = currentCount + 1;
// 返回状态更新(不是完整状态)
return {
counter: newCount,
messages: [new AIMessage(`计数:${newCount}`)],
};
}
返回值说明:
- 返回的是状态更新,不是完整状态
- LangGraph 会自动合并到当前状态
- 只返回需要改变的字段
Edge(边)
边连接节点,分为两种类型:
普通边(固定路由):
workflow.addEdge("nodeA", "nodeB");
// nodeA 执行完后,总是转到 nodeB
条件边(动态路由):
workflow.addConditionalEdges("agent", shouldContinue, {
tools: "tools",
__end__: END,
});
function shouldContinue(state) {
if (hasToolCalls(state)) {
return "tools"; // 转到 tools 节点
}
return END; // 结束
}
Graph(图)
图是所有节点和边的集合。
const workflow = new StateGraph(MyState)
.addNode("nodeA", myNodeA)
.addNode("nodeB", myNodeB)
.addEdge(START, "nodeA")
.addEdge("nodeA", "nodeB")
.addEdge("nodeB", END);
const app = workflow.compile(); // 编译为可执行对象
3、手动构建等效的 Agent 图
下面这段代码与 createAgent() 在功能上等效,但直接使用 LangGraph API。
完整实现
import {
StateGraph,
MessagesAnnotation,
END,
START
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage } from "@langchain/core/messages";
// 第一步:准备模型和工具
const model = new ChatOpenAI({ model: "gpt-4o" });
const tools = [searchTool, calculatorTool];
// 绑定工具到模型
const modelWithTools = model.bindTools(tools);
// 第二步:定义 agent 节点
async function agentNode(state: typeof MessagesAnnotation.State) {
// 调用绑定了工具的模型
const response = await modelWithTools.invoke(state.messages);
// 返回状态更新
return { messages: [response] };
}
// 第三步:定义条件判断函数
function shouldContinue(state: typeof MessagesAnnotation.State) {
const lastMessage = state.messages.at(-1) as AIMessage;
if (lastMessage.tool_calls?.length) {
return "tools"; // 有工具调用,转到 tools 节点
}
return END; // 没有工具调用,结束
}
// 第四步:构建图
const workflow = new StateGraph(MessagesAnnotation)
.addNode("agent", agentNode) // agent 节点
.addNode("tools", new ToolNode(tools)) // tools 节点
.addEdge(START, "agent") // 入口 → agent
.addConditionalEdges("agent", shouldContinue) // agent → tools 或 END
.addEdge("tools", "agent"); // tools → agent
// 第五步:编译为可执行对象
const app = workflow.compile();
// 第六步:执行
const result = await app.invoke({
messages: [{ role: "user", content: "2 + 3 等于多少?" }],
});
代码分步解读:
-
第 11-14 行:准备模型和工具
- 创建 ChatOpenAI 实例
- 定义工具列表
- 使用
bindTools()将工具绑定到模型
-
第 17-22 行:定义 agent 节点
- 调用模型,传入所有历史消息
- 返回模型响应(追加到消息列表)
-
第 25-32 行:定义条件判断
- 检查最后一条消息是否有工具调用
- 返回下一个节点的名称
-
第 35-40 行:构建图结构
- 添加两个节点:agent 和 tools
- 定义边的连接关系
-
第 43 行:编译
- 将图结构编译为可执行的 Runnable
-
第 46-48 行:执行
- 传入初始消息
- 获取最终结果
4、什么时候需要直接用 LangGraph?
大多数时候,createAgent() 就够用了。以下场景需要用 LangGraph:
| 场景 | 为什么需要 LangGraph | 示例 |
|---|---|---|
| 多 Agent 协作 | 需要定义多个 Agent 节点之间的路由逻辑 | 研究员 Agent → 作家 Agent → 编辑 Agent |
| 复杂条件分支 | 需要根据业务逻辑动态决定下一步 | 根据用户等级走不同流程 |
| Human-in-the-loop | 需要在特定节点暂停等待人工确认 | 删除操作前需要确认 |
| 子图(Subgraph) | 把复杂流程模块化为可复用的子图 | 订单处理子图、退款子图 |
| 自定义状态流转 | 默认的 Agent 循环无法满足需求 | 需要额外的状态字段和流转逻辑 |
决策建议:
flowchart TD
A["开始构建 Agent"] --> B{"是否需要<br/>自定义流程?"}
B -- 否 --> C["使用 createAgent()"]
B -- 是 --> D{"是否涉及<br/>多 Agent 协作?"}
D -- 否 --> E["使用 LangGraph<br/>单图"]
D -- 是 --> F["使用 LangGraph<br/>多图/子图"]
C --> G["快速开发"]
E --> H["灵活控制"]
F --> I["复杂编排"]
style C fill:#f6ffed,stroke:#52c41a,stroke-width:3px
style E fill:#fff7e6,stroke:#fa8c16,stroke-width:3px
style F fill:#e8f4fd,stroke:#1890ff,stroke-width:3px
四、中间件系统
中间件(Middleware) 是 LangChain.js v1.x 的重要新特性,允许你在 Agent 执行的不同阶段注入自定义逻辑,而不需要修改核心代码。
1、中间件的执行时机
flowchart LR
Input["用户输入"] --> BM
subgraph MiddlewarePipeline["中间件管道"]
BM["beforeModel<br/>模型调用前"] --> Model["LLM 调用"]
Model --> AM["afterModel<br/>模型调用后"]
end
AM --> TW
subgraph ToolExecution["工具执行"]
TW["wrapToolCall<br/>工具调用包装"]
end
TW --> Output["返回结果"]
style BM fill:#e8f4fd,stroke:#1890ff
style AM fill:#f6ffed,stroke:#52c41a
style TW fill:#fff0f6,stroke:#eb2f96
2、四种中间件钩子
钩子总览
| 钩子 | 执行时机 | 用途 | 可修改内容 |
|---|---|---|---|
beforeModel | LLM 调用前 | 日志、审核、限流 | 请求参数 |
afterModel | LLM 调用后 | 成本追踪、后处理 | 响应结果 |
wrapToolCall | 工具调用时 | 错误处理、性能监控 | 工具执行 |
wrapModelCall | 完整模型调用 | 重试、缓存 | 整个调用过程 |
完整示例
import { createMiddleware } from "langchain";
const loggingMiddleware = createMiddleware({
name: "Logger",
// 钩子 1:在 LLM 调用之前执行
beforeModel: async (request) => {
console.log(`[LLM] 调用模型,消息数量:${request.messages.length}`);
// 可以修改请求
// request.messages.push(new SystemMessage("额外提示"));
return request; // 返回(可修改的)request
},
// 钩子 2:在 LLM 调用之后执行
afterModel: async (response) => {
console.log(`[LLM] 模型响应,Token 用量:${response.usage?.total_tokens}`);
// 可以修改响应
// response.message.content += "\n\n[由助手生成]";
return response;
},
// 钩子 3:包装工具调用
wrapToolCall: async (request, handler) => {
const start = Date.now();
console.log(`[Tool] 调用工具:${request.toolCall.name}`);
try {
// 执行原始的工具调用
const result = await handler(request);
const duration = Date.now() - start;
console.log(`[Tool] 工具执行耗时:${duration}ms`);
return result;
} catch (error) {
console.error(`[Tool] 工具调用失败:`, error);
throw error;
}
},
// 钩子 4:包装完整的模型调用
wrapModelCall: async (request, handler) => {
console.log("[Model] 开始模型调用");
try {
const result = await handler(request);
console.log("[Model] 模型调用成功");
return result;
} catch (error) {
console.error("[Model] 模型调用失败:", error);
throw error;
}
},
});
// 注册中间件
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
middleware: [loggingMiddleware],
});
3、常用中间件模式
模式 1:成本控制
import { createMiddleware } from "langchain";
const costTracker = createMiddleware({
name: "CostTracker",
afterModel: async (response) => {
// 计算成本
const cost = calculateCost(response.usage, "gpt-4o");
// 记录到数据库
await saveCostRecord({
cost,
timestamp: new Date(),
userId: getCurrentUserId()
});
// 检查预算
if (exceedsBudget(cost)) {
throw new Error("超出预算限制");
}
return response;
},
});
function calculateCost(usage: any, model: string): number {
const pricing = {
"gpt-4o": { input: 0.005, output: 0.015 }, // 每 1K tokens
};
const price = pricing[model];
return (
(usage.prompt_tokens / 1000) * price.input +
(usage.completion_tokens / 1000) * price.output
);
}
模式 2:内容审核
const contentFilter = createMiddleware({
name: "ContentFilter",
beforeModel: async (request) => {
// 获取最新的用户消息
const lastUserMessage = request.messages.findLast(
m => m.role === "user"
);
if (lastUserMessage && containsViolation(lastUserMessage.content)) {
throw new Error("内容违规,请遵守使用条款");
}
return request;
},
});
function containsViolation(content: string): boolean {
// 简单的关键词过滤(生产环境用专业的审核服务)
const violations = ["暴力", "色情", "违法"];
return violations.some(word => content.includes(word));
}
模式 3:限流保护
import rateLimit from "express-rate-limit";
const rateLimiter = createMiddleware({
name: "RateLimiter",
beforeModel: async (request) => {
const userId = getCurrentUserId();
// 检查是否超限
await rateLimitCheck(userId);
return request;
},
});
async function rateLimitCheck(userId: string) {
// 实现限流逻辑
// 例如:每分钟最多 10 次调用
}
模式 4:响应缓存
const responseCache = createMiddleware({
name: "ResponseCache",
wrapModelCall: async (request, handler) => {
// 生成缓存键
const cacheKey = generateCacheKey(request.messages);
// 检查缓存
const cached = await cache.get(cacheKey);
if (cached) {
console.log("[Cache] 命中缓存");
return cached;
}
// 执行调用
const result = await handler(request);
// 存入缓存(有效期 1 小时)
await cache.set(cacheKey, result, { ttl: 3600 });
return result;
},
});
4、中间件的组合使用
可以同时注册多个中间件,它们会按顺序执行:
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
middleware: [
contentFilter, // 1. 内容审核
rateLimiter, // 2. 限流检查
loggingMiddleware, // 3. 日志记录
costTracker, // 4. 成本追踪
responseCache, // 5. 响应缓存
],
});
执行顺序:
- 内容审核(阻止违规请求)
- 限流检查(防止滥用)
- 日志记录(记录请求)
- 缓存检查(可能直接返回)
- 成本追踪(监控费用)
五、Human-in-the-loop:在关键节点引入人工确认
有些操作——比如删除数据、发送通知、执行付款——需要在执行前得到人工确认。LangGraph 的中断(Interrupt) 机制支持在任意节点暂停 Agent,等待外部输入。
1、为什么需要 Human-in-the-loop?
风险场景举例:
| 操作 | 风险 | 后果 |
|---|---|---|
| 删除数据 | 误删重要数据 | 数据丢失,无法恢复 |
| 发送邮件 | 发送给错误的收件人 | 信息泄露 |
| 执行付款 | 金额错误或重复支付 | 财务损失 |
| 部署代码 | 部署到有 Bug 的版本 | 服务中断 |
解决方案: 在关键操作前暂停,等待人工确认。
2、基本实现
第一步:定义需要确认的工具
import { interrupt, tool } from "langchain";
import { z } from "zod";
const deleteData = tool(
async ({ recordId }) => {
// 发起中断,等待人工确认
const confirmed = await interrupt({
question: `确认删除记录 ${recordId}?此操作不可撤销。`,
type: "confirm",
});
if (!confirmed) {
return "操作已取消";
}
// 执行删除
await db.delete(recordId);
return `记录 ${recordId} 已删除`;
},
{
name: "delete_data",
description: "删除指定记录(需要人工确认)",
schema: z.object({
recordId: z.string().describe("要删除的记录 ID")
}),
}
);
代码解读:
- 第 6-9 行:调用
interrupt()发起中断question:向用户展示的确认问题type:中断类型(confirm、input 等)
- 第 11-13 行:根据用户选择决定是否执行
- 第 16 行:只有确认后才会执行删除操作
第二步:创建 Agent
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model: "openai:gpt-4o",
tools: [deleteData],
checkpointer: new MemorySaver(), // ⚠️ Human-in-the-loop 需要 checkpointer
});
⚠️ 重要提示:
- Human-in-the-loop 必须配置
checkpointer- 因为中断后需要恢复状态,checkpointer 负责保存和恢复
第三步:执行并处理中断
const config = {
configurable: { thread_id: "session-001" }
};
// 第一次调用:Agent 会在需要确认时暂停
const result1 = await agent.invoke(
{ messages: [{ role: "user", content: "删除 ID 为 123 的记录" }] },
config
);
// 检查是否有中断
if (result1.interrupt) {
console.log(result1.interrupt.question);
// 输出:"确认删除记录 123?此操作不可撤销。"
// 向用户展示确认请求,获取用户选择
const userConfirmed = await askUserForConfirmation(
result1.interrupt.question
);
// 第二次调用:传入用户的确认结果
const result2 = await agent.invoke(
{ messages: [] }, // 不需要新消息
{
...config,
// 传入用户的确认结果
interrupt: { answer: userConfirmed }, // true 或 false
}
);
console.log(result2.messages.at(-1)?.content);
// 如果确认:输出 "记录 123 已删除"
// 如果取消:输出 "操作已取消"
}
执行流程:
sequenceDiagram
participant U as 用户
participant A as Agent
participant T as 删除工具
participant DB as 数据库
U->>A: "删除记录 123"
A->>T: 调用 delete_data(123)
T->>A: interrupt("确认删除?")
A-->>U: 显示确认对话框
U->>A: 点击"确认"
A->>T: resume(answer=true)
T->>DB: 执行删除
DB-->>T: 删除成功
T-->>A: "记录 123 已删除"
A-->>U: 返回结果
3、中断的类型
类型 1:确认型(confirm)
const confirmed = await interrupt({
question: "确认执行此操作?",
type: "confirm",
});
// 返回:true 或 false
类型 2:输入型(input)
const userInput = await interrupt({
question: "请输入验证码:",
type: "input",
});
// 返回:用户输入的字符串
类型 3:选择型(select)
const choice = await interrupt({
question: "请选择操作:",
type: "select",
options: ["继续", "暂停", "取消"],
});
// 返回:用户选择的选项
4、实际应用:审批工作流
const submitForApproval = tool(
async ({ documentId, approverId }) => {
// 发起审批中断
const approval = await interrupt({
question: `文档 ${documentId} 待审批,请决定:`,
type: "select",
options: ["批准", "拒绝", "需要修改"],
});
// 记录审批结果
await db.updateDocument(documentId, {
status: approval,
approvedBy: approverId,
approvedAt: new Date(),
});
return `文档 ${documentId} 已${approval}`;
},
{
name: "submit_for_approval",
description: "提交文档进行审批",
schema: z.object({
documentId: z.string(),
approverId: z.string(),
}),
}
);
六、本章小结
这一章深入了 Agent 的内部机制,从黑盒变成了白盒。
📝 核心知识点回顾
| 知识点 | 关键要点 | 应用场景 |
|---|---|---|
createAgent() 本质 | LangGraph 上的标准化 Agent 图 | 理解底层机制 |
| ReAct 模式 | 推理与行动交替执行的循环 | 需要多步推理的任务 |
| LangGraph 核心 | State / Node / Edge / Graph | 自定义复杂流程 |
| 中间件系统 | 在执行各阶段注入自定义逻辑 | 日志、成本控制、限流 |
| Human-in-the-loop | 在关键操作前暂停等待人工确认 | 高风险操作 |
🎯 动手练习
尝试完成以下练习,巩固所学知识:
练习 1:追踪 ReAct 执行过程 创建一个使用搜索工具的 Agent,执行一个需要多步推理的任务:
- 启用详细日志,观察每一轮的 Thought、Action、Observation
- 统计总共执行了多少轮
- 分析是否可以优化 Prompt 减少轮数
练习 2:手动构建 Agent 图
不使用 createAgent(),直接用 LangGraph API 构建一个等效的 Agent:
- 定义 agent 节点和 tools 节点
- 添加条件边
- 测试功能是否与
createAgent()一致
练习 3:编写成本控制中间件 实现一个成本追踪中间件:
- 记录每次调用的 Token 用量和费用
- 累计每日总成本
- 当超过预算时抛出错误
- 将成本数据保存到文件
练习 4:实现审批工作流 构建一个简单的文档审批系统:
- 用户上传文档
- Agent 分析文档内容
- 调用
submit_for_approval工具 - 等待人工审批(批准/拒绝/修改)
- 根据审批结果执行相应操作