欢迎光临
我们一直在努力

AI 应用开发实战(5):AI Agent —— 工具调用、自主决策、多步循环完整实现

前言

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

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

本篇从零构建一个 AI Agent 系统,涵盖:

  • 工具定义与注册机制
  • Function Calling 的完整实现
  • Agent 循环(Think → Act → Observe)
  • 多工具协作与错误恢复
  • 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 < self.max_iterations:
    iteration += 1

    if verbose:
    print(f"\\n{'='*40} Iteration {iteration} {'='*40}")

    # 调用 LLM
    response = self.client.chat.completions.create(
    model=os.getenv("LLM_MODEL", "deepseek-chat"),
    messages=messages,
    tools=self.registry.get_openai_tools(),
    tool_choice="auto",
    temperature=0.3, # Agent 场景用低温度,提高确定性
    )

    msg = response.choices[0].message

    if verbose:
    if msg.content:
    print(f" Thought: {msg.content[:200]}")
    if msg.tool_calls:
    for tc in msg.tool_calls:
    print(f" Action: {tc.function.name}({tc.function.arguments})")

    # 检查是否最终回答
    if not msg.tool_calls:
    return msg.content or "I'm not sure how to proceed."

    # 执行工具调用
    messages.append(msg)
    for tc in msg.tool_calls:
    tool_name = tc.function.name
    try:
    tool_args = json.loads(tc.function.arguments)
    except json.JSONDecodeError:
    tool_args = {}

    tool = self.registry.get(tool_name)
    if tool:
    if verbose:
    print(f" Executing: {tool_name}…")
    result = tool.execute(**tool_args)
    if verbose:
    print(f" Result: {result[:150]}…")
    else:
    result = f"Error: Unknown tool '{tool_name}'"

    messages.append({
    "role": "tool",
    "tool_call_id": tc.id,
    "content": str(result)[:2000],
    })

    # 超过最大迭代次数
    return "I've reached the maximum number of steps. Here's what I've found so far."

    3.3 Agent 执行示例

    用户:2023 年诺贝尔物理学奖得主是谁?他现在多少岁了?

    Agent 执行过程:

    Iteration 1:
    Thought: 我需要搜索 2023 年诺贝尔物理学奖的信息
    Action: web_search({"query": "2023 Nobel Prize in Physics winner"})
    Result: 2023 年诺贝尔物理学奖授予皮埃尔·阿戈斯蒂尼、费伦茨·克劳斯和安娜·吕利耶…

    Iteration 2:
    Thought: 我需要知道获奖者的出生年份来算年龄
    Action: web_search({"query": "Pierre Agostini birth date"})
    Result: Pierre Agostini born 1941

    Iteration 3:
    Thought: 知道了出生年份,计算当前年龄
    Action: calculator({"expression": "2026 – 1941"})
    Result: 2026 – 1941 = 85

    Final Answer: 2023 年诺贝尔物理学奖得主之一是皮埃尔·阿戈斯蒂尼,他出生于 1941 年,截至 2026 年已经 85 岁。

    4. 工具描述的艺术

    工具描述决定了 Agent 能不能正确调用工具。这是整个 Agent 系统最关键的设计点。

    4.1 好的描述 vs 差的描述

    # 差的描述——太模糊
    {
    "name": "search",
    "description": "搜索信息",
    }

    # 好的描述——明确说清楚什么时候该用、什么时候不该用
    {
    "name": "web_search",
    "description": "搜索互联网获取实时信息。当用户问及最新新闻、实时数据、\\
    当前事件、或你无法确定的事实时应使用此工具。\\
    不要用于简单的常识问题(如"中国的首都是什么"),直接回答即可。",
    }

    4.2 参数描述的关键

    # 差的参数描述
    {
    "query": {
    "type": "string",
    "description": "搜索关键词",
    }
    }

    # 好的参数描述——告诉 Agent 应该传什么内容、什么风格
    {
    "query": {
    "type": "string",
    "description": "搜索关键词。应该包含核心业务术语,\\
    如搜索「Python 异步编程最佳实践」而非「Python 怎么用 async」。\\
    使用精确的关键词组合,避免过于宽泛的词汇。",
    }
    }

    4.3 工具描述原则总结

    原则说明示例
    说清用途 明确工具能做什么 “执行数学计算,支持加减乘除、幂运算”
    说清边界 明确工具不能做什么 “不要用于文本处理或逻辑判断”
    说清时机 明确什么场景应该调用 “当需要实时信息时应使用此工具”
    说清参数 参数描述要具体 “搜索关键词,要求精确术语”

    5. 错误处理与容错

    Agent 在真实环境中一定会遇到各种错误。好的 Agent 应该能优雅地处理:

    class RobustAgent(Agent):
    """带错误处理的 Agent。"""

    def _execute_tool_safely(self, tool: BaseTool, args: dict) > 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” 循环:

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

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

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

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


    本文发布于 Zyentor(智元界) —— AI 开发者社区 原文链接:https://www.zyentor.com/news/3361

    赞(0)
    未经允许不得转载:171主机测评 » AI 应用开发实战(5):AI Agent —— 工具调用、自主决策、多步循环完整实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址