前言

前面四篇我们搭建了 AI 应用的基础设施——对话框架、Prompt 工程、多轮记忆、RAG 知识库。这一篇做最后的拼图:让 AI 不仅能"说话",还能"做事"

AI Agent 的核心能力就三个字:调用工具。当 AI 能调用搜索引擎查实时信息、调用计算器算数、调用 API 操作数据、调用代码解释器执行脚本时,它就不再只是一个聊天机器人,而是一个数字员工

本篇从零构建一个 AI Agent 系统,涵盖:
1. 工具定义与注册机制
2. Function Calling 的完整实现
3. Agent 循环(Think → Act → Observe)
4. 多工具协作与错误恢复

1. 什么是 AI Agent?

传统 LLM:   用户问 → LLM 回答(一次对话,无外部能力)
AI Agent:   用户问 → LLM 思考 → 调用工具 → 观察结果 → 再次思考 → ... → 最终回答

核心区别在于 Agent 可以自主决策调用什么工具、何时调用,并根据工具返回的结果调整后续行动。

┌─────────────────────────────────────────────────┐
│                 Agent 循环                       │
│                                                  │
│  Think → Act → Observe → Think → Act → ... → Done │
│  思考    行动    观察      再思考                 │
│                                                  │
│  Think:  分析当前状态,决定下一步做什么           │
│  Act:    调用工具或返回最终答案                   │
│  Observe: 获取工具执行结果                        │
│  Done:   达到目标或无法继续,返回最终答案          │
└─────────────────────────────────────────────────┘

2. 工具定义与注册

2.1 工具接口

首先定义工具的通用接口:

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
import json


class BaseTool(ABC):
    """工具的基类。"""

    @property
    @abstractmethod
    def name(self) -> str:
        """工具名称(在 Agent 中唯一标识)。"""
        pass

    @property
    @abstractmethod
    def description(self) -> str:
        """工具描述(Agent 根据描述决定是否调用此工具)。"""
        pass

    @property
    @abstractmethod
    def parameters(self) -> Dict:
        """工具参数 Schema(OpenAI Function Calling 格式)。"""
        pass

    @abstractmethod
    def execute(self, **kwargs) -> str:
        """执行工具,返回结果字符串。"""
        pass


class ToolRegistry:
    """工具注册中心。"""

    def __init__(self):
        self._tools: Dict[str, BaseTool] = {}

    def register(self, tool: BaseTool):
        """注册工具。"""
        self._tools[tool.name] = tool
        print(f"  Registered tool: {tool.name}")

    def get(self, name: str) -> Optional[BaseTool]:
        """获取工具。"""
        return self._tools.get(name)

    def get_all_tools(self) -> List[BaseTool]:
        """获取所有已注册的工具。"""
        return list(self._tools.values())

    def get_openai_tools(self) -> List[Dict]:
        """获取 OpenAI Function Calling 格式的工具定义。"""
        return [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.parameters,
                },
            }
            for tool in self._tools.values()
        ]

2.2 实现具体工具

计算器工具

import math
import re


class CalculatorTool(BaseTool):
    """计算器工具——执行数学运算。"""

    @property
    def name(self) -> str:
        return "calculator"

    @property
    def description(self) -> str:
        return "执行数学计算,支持加减乘除、幂运算、三角函数等。适用于需要精确计算的场景。"

    @property
    def parameters(self) -> Dict:
        return {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "数学表达式,如 '12 * 48 + 36 / 2'",
                }
            },
            "required": ["expression"],
        }

    def execute(self, expression: str = "") -> str:
        try:
            # 安全计算:只允许数学表达式
            allowed = re.sub(r"[0-9+\-*/.()% ]", "", expression)
            if allowed:
                return f"Error: Invalid characters: {allowed}"
            result = eval(expression, {"__builtins__": {}}, {"math": math})
            return f"{expression} = {result}"
        except Exception as e:
            return f"Calculation error: {e}"

搜索工具(需要搜索引擎 API):

import requests
import os


class WebSearchTool(BaseTool):
    """搜索引擎工具——搜索实时信息。"""

    @property
    def name(self) -> str:
        return "web_search"

    @property
    def description(self) -> str:
        return "搜索互联网获取实时信息。当需要最新新闻、实时数据、或不确定的事实时应使用此工具。"

    @property
    def parameters(self) -> Dict:
        return {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "搜索关键词,应具体且包含核心业务术语",
                },
                "count": {
                    "type": "integer",
                    "description": "返回结果数量(默认 5)",
                    "default": 5,
                },
            },
            "required": ["query"],
        }

    def execute(self, query: str = "", count: int = 5) -> str:
        try:
            api_key = os.getenv("SEARCH_API_KEY", "")
            if not api_key:
                return "Search API not configured"

            # 这里使用 Bing Search API 为例
            headers = {"Ocp-Apim-Subscription-Key": api_key}
            params = {"q": query, "count": count, "mkt": "zh-CN"}
            resp = requests.get(
                "https://api.bing.microsoft.com/v7.0/search",
                headers=headers,
                params=params,
                timeout=10,
            )
            results = resp.json()

            snippets = []
            for r in results.get("webPages", {}).get("value", [])[:count]:
                snippets.append(f"- [{r['name']}]({r['url']}): {r['snippet']}")
            return "\n".join(snippets) if snippets else "No results found"
        except Exception as e:
            return f"Search error: {e}"

文件读写工具

class FileReadTool(BaseTool):
    """文件读取工具——读取本地文件内容。"""

    @property
    def name(self) -> str:
        return "file_read"

    @property
    def description(self) -> str:
        return "读取本地文件内容。支持 txt、py、md、json 等文本格式。"

    @property
    def parameters(self) -> Dict:
        return {
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "文件路径(绝对路径或相对路径)",
                }
            },
            "required": ["file_path"],
        }

    def execute(self, file_path: str = "") -> str:
        try:
            # 安全检查:防止读取敏感文件
            forbidden = ["/etc/passwd", "/etc/shadow", ".env"]
            if any(f in file_path for f in forbidden):
                return "Error: Access denied"
            with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
            if len(content) > 5000:
                content = content[:5000] + "\n\n... (truncated)"
            return content
        except Exception as e:
            return f"Read error: {e}"

当前时间工具

import time


class CurrentTimeTool(BaseTool):
    """时间工具——获取当前日期和时间。"""

    @property
    def name(self) -> str:
        return "current_time"

    @property
    def description(self) -> str:
        return "获取当前日期和时间。当需要知道当前时间、日期、星期几时使用。"

    @property
    def parameters(self) -> Dict:
        return {
            "type": "object",
            "properties": {
                "format": {
                    "type": "string",
                    "description": "时间格式(可选:date/time/datetime/timestamp)",
                    "default": "datetime",
                }
            },
        }

    def execute(self, format: str = "datetime") -> str:
        now = time.time()
        formats = {
            "date": time.strftime("%Y-%m-%d"),
            "time": time.strftime("%H:%M:%S"),
            "datetime": time.strftime("%Y-%m-%d %H:%M:%S"),
            "timestamp": str(int(now)),
        }
        return formats.get(format, formats["datetime"])

2.3 注册工具

registry = ToolRegistry()
registry.register(CalculatorTool())
registry.register(CurrentTimeTool())
registry.register(WebSearchTool())
registry.register(FileReadTool())

# 查看所有已注册工具
for t in registry.get_all_tools():
    print(f"  {t.name}: {t.description[:50]}...")

3. Agent 核心循环

3.1 单步 Agent

最简单的 Agent——只做一次工具调用决策:

from openai import OpenAI


class SimpleAgent:
    """单步 Agent:接收用户输入,决定是否调用工具,返回结果。"""

    def __init__(self, registry: ToolRegistry, llm_client: OpenAI):
        self.registry = registry
        self.client = llm_client
        self.system_prompt = """你是一个 AI 助手,可以使用工具来帮助用户。
在回答问题时,如果需要实时信息或精确计算,请使用对应工具。
如果不需要工具,直接回答即可。"""

    def run(self, user_input: str) -> str:
        """处理用户输入。"""
        messages = [{"role": "system", "content": self.system_prompt}]
        messages.append({"role": "user", "content": user_input})

        response = self.client.chat.completions.create(
            model=os.getenv("LLM_MODEL", "deepseek-chat"),
            messages=messages,
            tools=self.registry.get_openai_tools(),
            tool_choice="auto",
        )

        msg = response.choices[0].message

        # 如果模型决定调用工具
        if msg.tool_calls:
            for tc in msg.tool_calls:
                tool_name = tc.function.name
                tool_args = json.loads(tc.function.arguments)
                tool = self.registry.get(tool_name)

                if tool:
                    # 执行工具
                    result = tool.execute(**tool_args)
                    messages.append(msg)
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": result,
                    })

            # 调用 LLM 生成最终回答
            final = self.client.chat.completions.create(
                model=os.getenv("LLM_MODEL", "deepseek-chat"),
                messages=messages,
            )
            return final.choices[0].message.content

        # 不需要工具,直接返回
        return msg.content

3.2 多步 Agent(完整 Agent 循环)

真正的 Agent 需要多步循环——一次工具调用的结果可能触发下一次工具调用:

class Agent:
    """完整 Agent:支持多步工具调用循环。"""

    def __init__(
        self,
        registry: ToolRegistry,
        llm_client: OpenAI,
        max_iterations: int = 10,
    ):
        self.registry = registry
        self.client = llm_client
        self.max_iterations = max_iterations

        self.system_prompt = """你是一个能使用工具的 AI 助手。

## 行为规则
1. 分析用户需求,决定是否使用工具
2. 一次调用一个工具,等待结果后再决定下一步
3. 工具返回结果后,分析结果是否满足需求
4. 如果满足,给出最终答案
5. 如果不满足或需要更多信息,继续调用其他工具
6. 如果尝试多次仍无法解决,告知用户当前进展和遇到的困难
7. 所有数字计算必须使用 calculator 工具,不要自己算

## 可用工具
{tools_description}

请按以下格式思考:
Thought: 分析当前情况,决定下一步做什么
Action: 工具名称(如果不需工具则输出 Final Answer)
Action Input: 工具参数 JSON
"""

    def run(self, user_input: str, verbose: bool = True) -> str:
        """运行 Agent。"""
        tools_desc = "\n".join([
            f"- {t.name}: {t.description}(参数:{json.dumps(t.parameters, ensure_ascii=False)})"
            for t in self.registry.get_all_tools()
        ])

        messages = [
            {"role": "system", "content": self.system_prompt.format(
                tools_description=tools_desc
            )},
            {"role": "user", "content": user_input},
        ]

        iteration = 0
        while iteration  str:
        """安全执行工具,捕获所有异常。"""
        try:
            return tool.execute(**args)
        except TypeError as e:
            # 参数错误——Agent 传了错误的参数
            return f"ParameterError: {e}. Please check the required parameters and types."
        except TimeoutError:
            return "TimeoutError: The tool execution timed out."
        except PermissionError:
            return "PermissionError: Access denied."
        except Exception as e:
            return f"UnexpectedError: {type(e).__name__}: {e}"

    def _handle_tool_error(self, error_msg: str, messages: list) -> bool:
        """判断工具错误是否可恢复。"""
        recoverable = [
            "ParameterError",
            "TimeoutError",
        ]
        return any(e in error_msg for e in recoverable)

6. 与前面几篇的集成

将 Agent 集成到系列第一篇的 AI Chat 应用中:

# agent_app.py — 在 AI Chat 中添加 Agent 模式

from agent_core import Agent, ToolRegistry
from tools import (
    CalculatorTool, CurrentTimeTool,
    WebSearchTool, FileReadTool,
)
from openai import OpenAI
import os

# 初始化 LLM 客户端
client = OpenAI(
    api_key=os.getenv("LLM_API_KEY"),
    base_url=os.getenv("LLM_BASE_URL"),
)

# 注册工具
registry = ToolRegistry()
registry.register(CalculatorTool())
registry.register(CurrentTimeTool())
registry.register(WebSearchTool())
registry.register(FileReadTool())

# 创建 Agent
agent = Agent(registry, client)


@app.post("/api/agent/chat")
async def agent_chat(request: Request):
    """Agent 对话接口。"""
    body = await request.json()
    user_message = body.get("message", "")
    session_id = body.get("session_id", "default")

    if not user_message.strip():
        return JSONResponse({"error": "Message required"}, status_code=400)

    # 使用 Agent 处理
    try:
        result = agent.run(user_message, verbose=False)
        return JSONResponse({
            "reply": result,
            "mode": "agent",
        })
    except Exception as e:
        return JSONResponse({
            "reply": f"Agent error: {e}",
            "mode": "agent",
        })


@app.get("/api/agent/tools")
async def list_tools():
    """查看可用的 Agent 工具列表。"""
    tools = [
        {"name": t.name, "description": t.description}
        for t in registry.get_all_tools()
    ]
    return JSONResponse({"tools": tools})

7. Agent 应用的三大模式

根据使用场景,Agent 有三种常见模式:

模式 1:Chat Agent(对话式)
  用户  Agent  Tools
  适合:客服、助手、知识问答

模式 2:Workflow Agent(工作流式)
  用户 -> Agent -> Tool1 -> Agent -> Tool2 -> Agent -> 结果
  适合:数据处理、报告生成、多步骤操作

模式 3:Orchestrator Agent(编排式)
         ┌─ Agent A ─ Tool A
 用户 ── Orchestrator ── Agent B ─ Tool B ── 最终结果
         └─ Agent C ─ Tool C
  适合:复杂任务分解、多 Agent 协作

本文实现的是模式 1(Chat Agent),它是最通用、最稳定的模式。

8. 注意事项与最佳实践

8.1 Token 消耗

Agent 循环会消耗大量 Token。每次迭代都包含:历史消息 + 工具定义 + 工具调用 + 工具结果。

# 每次 Agent 调用的 Token 构成
total_tokens = (
    system_prompt_tokens    # 包含所有工具定义
    + history_tokens        # 历史对话
    + tool_call_tokens      # 模型生成的 tool call
    + tool_result_tokens    # 工具返回的结果
    + final_output_tokens   # 最终回答
)

# 估算:一轮 Agent 循环约 2000-5000 tokens
# 5 轮循环约 10000-25000 tokens

优化建议
- 限制 max_iterations(建议 5-10 轮)
- 工具结果只保留关键字段(用摘要代替完整输出)
- 定期清理历史消息(参考第3篇的上下文截断策略)

8.2 安全注意事项

# 工具调用的安全清单
SAFETY_CHECKLIST = """
□ 文件读写工具:限制可访问的目录范围
□ 网络工具:限制可访问的域名/IP
□ 执行工具:禁止执行 shell 命令(除非明确需要)
□ 敏感操作:需要用户确认才能执行
□ 数据隐私:工具结果中不要泄露敏感信息
"""

8.3 何时应该用 Agent?

适合 Agent 不适合 Agent
需要搜索实时信息 纯知识问答(RAG 更适合)
需要精确计算 简单对话
多步骤操作 单步任务
需要操作外部系统 需要极低延迟
不确定的复杂任务 高安全/高可靠性要求

总结

AI Agent 的核心是 "Think → Act → Observe" 循环

  1. 工具是关键——Agent 的能力边界由工具决定,不是由模型决定
  2. 描述是灵魂——工具描述的质量直接影响 Agent 的决策准确性
  3. 循环是引擎——多步循环让 Agent 能完成复杂任务
  4. 容错是保障——真实环境中错误必然发生,优雅处理很重要

至此,《AI 应用开发实战》系列五篇全部完成 🎉

篇目 标题
第1篇 从零搭建你的第一个 AI 应用
第2篇 Prompt 工程实战
第3篇 多轮对话进阶
第4篇 从零实现 RAG 系统
第5篇 AI Agent——工具调用与自主决策 ← 你在这里

本文是 《AI 应用开发实战》系列 的第 5 篇。
本文由 Zyentor(智元界) 原创发布