主题
示例 01 单 Agent 工具调用最小闭环
本示例对应主线第 3 章《工具调用与单智能体》。目标不是做一个很复杂的 Agent,而是给你一套前后端都能快速搭起来的最小骨架:
- 模型根据用户输入决定是否调用工具。
- 工具返回结构化结果。
- 前端按明确状态展示执行过程。
- 失败时有基础兜底。
1. 场景目标
示例场景:用户问“帮我查一下今天上海天气,并给我一句出门建议”。 系统需要:
- 识别是否需要调用天气工具。
- 调用工具并拿到结构化结果。
- 返回可消费的回答结构。
- 前端展示
thinking -> calling_tool -> completed过程。
2. Python 服务端最小骨架
python
from dataclasses import dataclass
from typing import Any
@dataclass
class ToolCall:
name: str
arguments: dict[str, Any]
TOOLS = {
"get_weather": {
"description": "查询指定城市天气",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
},
}
}
def decide_tool(user_input: str) -> ToolCall | None:
if "天气" in user_input:
return ToolCall(name="get_weather", arguments={"city": "上海"})
return None
def run_tool(tool_call: ToolCall) -> dict[str, Any]:
if tool_call.name == "get_weather":
return {
"city": tool_call.arguments["city"],
"condition": "多云",
"temperature_c": 24,
}
raise ValueError("unknown tool")
def build_response(user_input: str) -> dict[str, Any]:
tool_call = decide_tool(user_input)
if not tool_call:
return {
"answer": "当前请求不需要工具调用。",
"tool_used": None,
"tool_result": None,
"need_human_review": False,
}
tool_result = run_tool(tool_call)
answer = f"{tool_result['city']}当前{tool_result['condition']},气温约{tool_result['temperature_c']}°C,建议轻便出行。"
return {
"answer": answer,
"tool_used": tool_call.name,
"tool_result": tool_result,
"need_human_review": False,
}2.5 Node/TS 服务端(Vercel AI SDK)
示例基于
ai@^3.x+@ai-sdk/openai,验证于 2026-03。 可运行代码见examples/example-01-single-agent/api-ts/。
ts
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";
import { createServer } from "node:http";
// --- 工具定义 ---
const weatherTool = tool({
description: "查询指定城市天气",
parameters: z.object({
city: z.string().describe("城市名称"),
}),
execute: async ({ city }) => ({
city,
condition: "多云",
temperature_c: 24,
}),
});
// --- AgentResponse:与 Python 端保持一致 ---
interface AgentResponse {
answer: string;
tool_used: string | null;
tool_result: Record<string, unknown> | null;
need_human_review: boolean;
}
// --- 核心:streamText + tool 调用 ---
async function buildResponse(input: string): Promise<AgentResponse> {
const result = await streamText({
model: openai("gpt-4o-mini"),
tools: { get_weather: weatherTool },
maxSteps: 3,
prompt: input,
});
let answer = "";
let toolUsed: string | null = null;
let toolResult: Record<string, unknown> | null = null;
for await (const part of result.fullStream) {
if (part.type === "text-delta") {
answer += part.textDelta;
} else if (part.type === "tool-call") {
toolUsed = part.toolName;
} else if (part.type === "tool-result") {
toolResult = part.result as Record<string, unknown>;
}
}
return {
answer: answer.trim(),
tool_used: toolUsed,
tool_result: toolResult,
need_human_review: false,
};
}
// --- HTTP 服务:POST /api/agent ---
createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") { res.writeHead(204).end(); return; }
if (req.method === "GET" && req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
if (req.method !== "POST" || req.url !== "/api/agent") {
res.writeHead(404).end(JSON.stringify({ error: "not_found" }));
return;
}
const body = await new Promise<string>((r) => {
let d = "";
req.on("data", (c) => (d += c));
req.on("end", () => r(d));
});
const { input } = JSON.parse(body || "{}");
const data = await buildResponse(input ?? "");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}).listen(8102, "127.0.0.1", () => {
console.log("example-01 api-ts: http://127.0.0.1:8102/health");
});
// 纯 fetch + OpenAI API 等价思路:
// 用 fetch 调 https://api.openai.com/v1/chat/completions,
// 在 body.tools 里声明 get_weather function schema,
// 收到 tool_calls 后本地执行再续发 tool role message 即可。与 Python 端完全一致的请求 / 响应形态:
bash
curl -X POST http://127.0.0.1:8102/api/agent \
-H "Content-Type: application/json" \
-d '{"input":"帮我查一下今天上海天气"}'
# => {"answer":"...","tool_used":"get_weather","tool_result":{...},"need_human_review":false}3. 前端状态与响应结构
3.1 TypeScript 状态枚举
ts
export type AgentStatus =
| 'idle'
| 'thinking'
| 'calling_tool'
| 'completed'
| 'failed';3.2 响应结构示例
ts
export interface AgentResponse {
answer: string;
tool_used: string | null;
tool_result: Record<string, unknown> | null;
need_human_review: boolean;
}3.3 React 侧最小状态切换
ts
const [status, setStatus] = useState<AgentStatus>('idle');
const [result, setResult] = useState<AgentResponse | null>(null);
async function submitQuestion(input: string) {
setStatus('thinking');
const response = await fetch('/api/agent', {
method: 'POST',
body: JSON.stringify({ input }),
});
setStatus('calling_tool');
const data = (await response.json()) as AgentResponse;
setResult(data);
setStatus('completed');
}4. 前端应该展示什么
这个最小页面建议至少展示:
- 用户输入框。
- 当前状态标签。
- 最终回答。
- 调用的工具名。
- 工具结果摘要。
- 出错时的空态或错误提示。
5. 常见扩展点
在这个骨架基础上,你可以继续扩展:
- 多工具路由。
- 工具调用日志。
- 高风险工具确认机制。
- 结构化输出校验。
- 工具失败后的回退提示。
6. 截图建议
建议保留以下 4 类截图,后续作品集很好用:
- 初始输入页。
thinking / calling_tool中间状态。- 完成后的回答和工具结果。
- 失败或空态场景。
对应命名可参考 docs/examples/assets/README.md。