Skip to content

示例 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