第05章:AI Agent 智能体 —— 让 AI 自主决策,循环解决问题

8 阅读22分钟

本章目标:深入理解 ReAct 推理模式,掌握用 create_agent(LangGraph V1.0 起从 langchain.agents 导入,原名 create_react_agent)快速构建 Agent、手动实现 Agent 循环、多工具 Agent 设计,以及 Agent 与 Chain 的选型判断。

前期回顾


🚀 写给零基础开发者(5分钟读懂 Agent)

1.1 什么是 Agent?用一个比喻理解

想象你在指挥一个助理完成一项任务:"帮我计划一次北京→上海的出差,整理日程表并发送邮件。"

普通程序员(没有 Agent)的做法

步骤1:我写代码查询机票 → 步骤2:我写代码查询酒店 → 步骤3:我手动整合结果 → 步骤4:我写代码发邮件
每一步都要我预先知道做什么,预先写好代码,无法应对变化。

有了 Agent 之后

用户说:"帮我计划出差" → Agent 自主决定:先查机票 → 根据机票结果查酒店 → 发现冲突就重新搜索 → 整合信息 → 发邮件
AI 自己决定步骤,自己执行,自己检查,直到完成任务。

一句话总结:Agent = 能自主思考 + 主动调用工具 + 循环改进直到完成任务的 AI 助理。


1.2 为什么需要 Agent?没有它会有什么问题?

❌ 只用 LLM(无 Agent/无工具):能力有限、结果不可信

# 纯 LLM 对话,没有工具,没有 Agent
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="qwen-plus", ...)

# 问题1:实时数据 → LLM 只能猜测
response = llm.invoke("今天北京天气怎么样?")
# 输出:❌ "今天北京天气晴朗..." (LLM 瞎编的!它根本不知道今天天气)

# 问题2:精确计算 → LLM 会出错
response = llm.invoke("sqrt(256) × 17 + 2^8 是多少?")
# 输出:❌ 可能是 "4352"(大模型数学常出错,无法保证精确)

# 问题3:多步骤开放性任务 → LLM 无法真正执行
response = llm.invoke("查询一下我公司的最新销售数据,然后生成报表")
# 输出:❌ "我无法访问你公司的数据..." (LLM 根本没有工具访问能力)

❌ 只用硬编码程序(无 LLM):灵活性极差

# 假设你用 if-else 写了一个"智能"助手
def smart_assistant(user_input: str) -> str:
    if "天气" in user_input:
        return query_weather()    # 只能处理天气
    elif "计算" in user_input:
        return do_calculation()   # 只能处理计算
    else:
        return "我不理解你的问题"  # 稍微变个说法就失败

# 用户说:"现在几度?需要带伞吗?"
# ❌ 程序不认识"几度"这个词,无法处理

# 用户说:"帮我算一下出发前还有几天,然后查一下目的地天气,决定带哪件外套"
# ❌ 多步骤任务完全无法处理(需要先算日期,再查天气,再决策)

✅ LLM + Agent:两者优点结合

用户输入 → LLM 理解意图(自然语言理解)
         → Agent 自主决定调用哪些工具
         → 工具执行真实操作(精确、实时)
         → LLM 分析结果、决定下一步
         → 循环直到完成任务
         → 输出可信的最终答案

总结对比表:

维度只用 LLM只用代码LLM + Agent
自然语言理解✅ 强❌ 弱✅ 强
实时数据获取❌ 不支持✅ 支持✅ 支持
精确计算⚠️ 不可靠✅ 精确✅ 精确
处理未知任务⚠️ 有限❌ 需重写代码✅ 自动路由
多步骤动态任务❌ 无法规划❌ 需预先设计✅ 自主规划
适应语言变化✅ 强❌ 弱✅ 强
错误自我修复❌ 不支持❌ 不支持✅ 支持

1.3 什么场景该用 Agent?(4题自测)

遇到以下问题,答"是"就需要 Agent:

  • ❓ 任务完成的步骤数是不确定的吗?(比如:可能需要 2 步,也可能需要 5 步)
  • ❓ 任务需要访问实时数据(天气、股价、数据库)吗?
  • ❓ 任务需要自主决策(根据上一步结果决定下一步)吗?
  • ❓ 任务可能需要在失败后自动重试或换一种方式吗?

答了 2 个以上"是" → 考虑使用 Agent

05-1.png


1.4 典型 Agent 使用场景

场景举例为什么用 Agent
数据分析助手"分析过去一年的销售数据,找出下滑原因"步骤不确定:可能需要多次查询、比对
智能客服"查询订单状态,计算退款,发通知"多系统工具调用,顺序动态决定
代码助手"写一个函数,测试,如有错误修复"需要执行→观察→改进的循环
研究助手"调研竞品,总结优缺点,写报告"搜索→阅读→分析→撰写,步骤不固定
任务规划"安排下周出差行程,订机票酒店"多工具、多决策、条件判断
数据管道"从数据库取数,清洗,存到新表"需要根据数据质量动态调整清洗步骤

不适合 Agent 的场景:

  • 固定格式的文本生成(用 Chain 更快更稳定)
  • 单次查询问答(直接用 LLM 即可)
  • 确定步骤的工作流(用 Chain + Tools)

一、什么是 Agent?和 Chain 有什么区别?

在第03章,我们学了 Chain:预先定义好执行步骤,输入进来,按固定顺序走完所有步骤,输出结果。Chain 很适合确定性任务(如:翻译→润色→输出),但面对开放性问题就力不从心了。

Agent(智能体) 则完全不同:

  • Chain:决定执行哪些步骤,执行几次
  • Agent:LLM 决定执行哪些步骤,执行几次,何时停止
flowchart TD
    subgraph Chain["Chain(确定性流程)"]
        C1[步骤1: 翻译] --> C2[步骤2: 润色] --> C3[步骤3: 输出]
    end
    
    subgraph Agent["Agent(自主决策循环)"]
        A1[LLM思考] --> A2{需要工具?}
        A2 -->|是| A3[执行工具]
        A3 --> A4[观察结果]
        A4 --> A1
        A2 -->|任务完成| A5[输出答案]
    end

Agent 内部消息流详解:

sequenceDiagram
    participant U as 用户
    participant A as Agent
    participant L as LLM (大脑)
    participant T as 工具 (手)

    U->>A: "今天距离春节还有多少天?"
    A->>L: 发送问题 + 可用工具列表
    L->>A: 决策:调用 get_current_time 工具
    A->>T: 执行 get_current_time()
    T->>A: "2025年03月10日,周一"
    A->>L: 发送工具结果
    L->>A: 决策:调用 date_calculator 工具
    A->>T: date_calculator(start="2025-03-10", end="2026-01-29")
    T->>A: "相差 325 天,约 46 周"
    A->>L: 发送工具结果
    L->>A: 任务完成,给出最终答案
    A->>U: "距离2026年春节还有325天,约46周"

什么时候用 Agent?什么时候用 Chain?

场景推荐方案原因
格式转换、文字处理Chain步骤确定,Chain 更可靠、更快
复杂问答(需检索)Agent需要决定用哪些工具,几步才够
多步骤数学问题Agent步骤数不确定
代码调试Agent需要执行-观察-修复的循环
固定报告生成Chain每次都是同样的步骤

二、ReAct 模式:AI 的"思考-行动-观察"循环

ReAct(Reasoning + Acting)是目前最主流的 Agent 模式,由普林斯顿大学 2022 年提出。

它的核心是三个步骤的循环:

Thought(思考): 我需要知道 3 × 17 的结果和今天的日期
Action(行动): calculator.invoke({"expression": "3 * 17"})
Observation(观察): 51

Thought(思考): 已知乘法结果是 51,现在获取日期
Action(行动): get_current_time.invoke({})
Observation(观察): 2025年03月15日

Thought(思考): 我现在有了所有需要的信息
Final Answer(最终答案): 3 乘以 17 等于 51。今天是 2025年3月15日。

这个模式为什么有效?因为它将"推理"和"行动"明确分开,每次行动后都基于真实结果重新推理,避免了 LLM 的"幻觉"(凭空编造)。


三、Step 1:快速创建 ReAct Agent

代码文件:lessons/05_agents/01_react_agent.py

运行命令:

cd ai-agent-test
python lessons/05_agents/01_react_agent.py

核心步骤:定义工具 → 创建 LLM → 组装 Agent → 运行

import math
import os
import sys
from datetime import datetime

from langchain_core.messages import HumanMessage
from langchain_core.tools import tool          # @tool 装饰器:把函数变成 LLM 可调用的工具
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent  # 核心:一行创建 ReAct Agent(LangGraph V1.0 起推荐从 langchain.agents 导入,原名 create_react_agent)
from pydantic import BaseModel, Field


# ── 步骤1:初始化 LLM ──────────────────────────────────────────
def create_llm() -> ChatOpenAI:
    """创建百炼 API 实例。"""
    api_key = os.getenv("DASHSCOPE_API_KEY")
    if not api_key:
        print("错误:请设置环境变量 DASHSCOPE_API_KEY")
        sys.exit(1)

    return ChatOpenAI(
        model="qwen-plus",
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
        api_key=api_key,
        # temperature 不设置(默认),Agent 本身会控制推理的确定性
    )


# ── 步骤2:定义工具(工具是 Agent 的"手")──────────────────────

# 关键:工具的 docstring 非常重要!LLM 会读这段描述来决定"什么时候用这个工具"
class CalculatorInput(BaseModel):
    # Field 的 description 帮助 LLM 理解参数含义
    expression: str = Field(description="数学表达式,如 '2 + 3'、'sqrt(16)'、'2 ** 10'")


@tool(args_schema=CalculatorInput)  # args_schema 让 LLM 知道参数结构
def calculator(expression: str) -> str:
    """
    计算数学表达式。
    支持基本运算(+、-、*、/)、幂运算(**)、
    以及常见数学函数:sqrt(平方根)、abs(绝对值)、round(四舍五入)。
    """
    try:
        # 安全的 eval:只允许数学函数,禁止执行系统命令
        safe_globals = {
            "__builtins__": {},       # 禁用所有内置函数(防止注入攻击)
            "sqrt": math.sqrt,        # 只开放数学函数
            "abs": abs,
            "round": round,
            "pow": pow,
            "pi": math.pi,
            "e": math.e,
            "log": math.log,
            "sin": math.sin,
            "cos": math.cos,
        }
        result = eval(expression, safe_globals)
        return f"{expression} = {result}"   # 返回值要清晰,LLM 会读取并解读
    except Exception as e:
        return f"计算错误:{str(e)}"


@tool
def get_current_datetime() -> str:
    """
    获取当前的日期和时间。
    返回格式:年-月-日 时:分:秒,以及星期几。
    当用户询问"现在几点"、"今天几号"、"今天星期几"时使用此工具。
    """
    now = datetime.now()
    weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
    weekday = weekdays[now.weekday()]
    return f"当前时间:{now.strftime('%Y年%m月%d日 %H:%M:%S')}{weekday}"


class StringOpsInput(BaseModel):
    text: str = Field(description="要操作的文本")
    operation: str = Field(
        description="操作类型:'upper'(大写)、'lower'(小写)、'reverse'(反转)、"
                    "'length'(长度)、'count_words'(统计词数)、'title'(标题格式)"
    )


@tool(args_schema=StringOpsInput)
def string_operations(text: str, operation: str) -> str:
    """
    对字符串执行各种操作:转换大小写、反转、统计长度等。
    当用户需要对文本进行格式化、分析或变换时使用。
    """
    ops = {
        "upper": lambda t: t.upper(),
        "lower": lambda t: t.lower(),
        "reverse": lambda t: t[::-1],
        "length": lambda t: str(len(t)),
        "count_words": lambda t: str(len(t.split())),
        "title": lambda t: t.title(),
    }

    if operation not in ops:
        return f"不支持的操作:{operation}。支持:{', '.join(ops.keys())}"

    result = ops[operation](text)
    return f"操作 '{operation}' 的结果:{result}"


# ── 步骤3:展示 Agent 推理过程(学习和调试用)────────────────────
def show_agent_steps(result: dict):
    """展示 Agent 每一步做了什么,帮助你理解 ReAct 循环。"""
    messages = result.get("messages", [])
    print(f"\n=== Agent 执行过程(共 {len(messages)} 条消息)===")

    for i, msg in enumerate(messages):
        msg_type = msg.__class__.__name__

        if msg_type == "HumanMessage":
            # 用户输入
            print(f"\n[{i+1}] 👤 用户:{msg.content}")

        elif msg_type == "AIMessage":
            if msg.tool_calls:
                # LLM 决定调用某个工具(ReAct 的 Action 步骤)
                for tc in msg.tool_calls:
                    print(f"\n[{i+1}] 🤖 AI(决策):决定调用工具 {tc['name']},参数:{tc['args']}")
            else:
                # LLM 认为已完成,给出最终答案(ReAct 的 Final Answer)
                print(f"\n[{i+1}] 🤖 AI(最终回答):{msg.content}")

        elif msg_type == "ToolMessage":
            # 工具执行结果(ReAct 的 Observation 步骤)
            content_preview = msg.content[:200]
            print(f"\n[{i+1}] 🔧 工具结果(Observation):{content_preview}")


# ── 步骤4:组装并运行 Agent ──────────────────────────────────────
def main():
    llm = create_llm()

    # 注册工具:Agent 会知道这些工具的名称、描述和参数
    tools = [calculator, string_operations, get_current_datetime]

    print("创建 ReAct Agent...")
    print(f"可用工具:{[t.name for t in tools]}")

    # create_agent 是快捷方式,一行创建完整的 ReAct Agent
    # LangGraph V1.0 起推荐:from langchain.agents import create_agent(原名 create_react_agent)
    # 内部自动处理:工具绑定、消息历史、循环逻辑、终止判断
    agent = create_agent(
        llm,       # 大脑:负责推理和决策
        tools=tools,  # 手:执行具体操作
    )

    # ── 示例1:单工具任务(简单路由)────────────────────────────
    print("\n" + "=" * 60)
    print("示例1:简单数学计算(Agent 会选择 calculator 工具)")
    print("=" * 60)

    result = agent.invoke({
        "messages": [HumanMessage(content="计算 sqrt(144) + 2**8 的结果")]
    })
    show_agent_steps(result)
    print(f"\n✅ 最终答案:{result['messages'][-1].content}")

    # ── 示例2:多步骤任务(Agent 自主规划步骤)──────────────────
    print("\n" + "=" * 60)
    print("示例2:多步骤推理(Agent 会调用多个工具)")
    print("=" * 60)

    result = agent.invoke({
        "messages": [HumanMessage(
            content="现在几点了?另外帮我算一下:如果我从现在开始跑步2.5小时,"
                    "每小时跑8公里,我能跑多少公里?"
        )]
    })
    show_agent_steps(result)
    print(f"\n✅ 最终答案:{result['messages'][-1].content}")


if __name__ == "__main__":
    main()

关键设计原则解析:

  1. 工具 docstring 要清晰:LLM 通过读 docstring 决定调用哪个工具,描述越准确,路由越正确
  2. 使用 args_schema(Pydantic):让 LLM 知道参数格式,减少参数传递错误
  3. 工具返回值要有意义:返回 "结果:42""42" 更好,LLM 能更好理解上下文
  4. 安全计算eval 时设置 {"__builtins__": {}} 禁用内置函数,防止代码注入

四、Step 2:理解 Agent 的内部机制(手动实现循环)

代码文件:lessons/05_agents/02_tool_calling_agent.py

create_agent(原名 create_react_agent,LangGraph V1.0 起更名)帮我们封装了所有细节。理解内部机制对调试和高级定制非常重要:

import os
import sys
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI


class ManualAgent:
    """
    手动实现的 Agent 循环,展示每一步的内部工作机制。
    这和 create_agent 内部做的事情完全一样,只是我们自己写出来看清楚。
    """

    def __init__(self, llm: ChatOpenAI, tools: list, max_iterations: int = 10):
        # bind_tools:告诉 LLM "你有这些工具可以调用"
        # 调用后 LLM 的响应中会包含 tool_calls 字段
        self.llm_with_tools = llm.bind_tools(tools)
        # 建立工具名称 → 工具对象的映射,方便快速查找
        self.tool_map = {t.name: t for t in tools}
        self.max_iterations = max_iterations  # 防止无限循环

    def run(self, user_message: str, system_prompt: str = None, verbose: bool = True) -> str:
        """运行完整的 Agent 循环,直到任务完成或超过最大迭代次数。"""
        
        # 初始化消息历史(这是 Agent 的"记忆")
        messages = []
        if system_prompt:
            # SystemMessage:定义 Agent 的角色和行为准则
            messages.append(SystemMessage(content=system_prompt))
        messages.append(HumanMessage(content=user_message))  # 用户输入

        if verbose:
            print(f"🎯 任务:{user_message}\n")

        # ============================================================
        # Agent 核心循环(这就是 ReAct 的实现)
        # ============================================================
        for iteration in range(self.max_iterations):
            if verbose:
                print(f"--- 第 {iteration + 1} 轮推理 ---")

            # ── 步骤A:调用 LLM 推理 ──────────────────────────────
            # LLM 收到消息历史,决定:调用某个工具?还是直接给出答案?
            response = self.llm_with_tools.invoke(messages)
            messages.append(response)  # 把 AI 响应加入历史(很重要!)

            # ── 步骤B:检查是否有工具调用请求 ─────────────────────
            if not response.tool_calls:
                # tool_calls 为空 → LLM 认为任务已完成,直接给答案
                if verbose:
                    print(f"✅ AI 给出最终答案(无需更多工具)")
                return response.content  # 返回最终答案

            # ── 步骤C:执行 LLM 请求的工具 ─────────────────────────
            if verbose:
                print(f"🔧 LLM 决定调用 {len(response.tool_calls)} 个工具:")

            for tool_call in response.tool_calls:
                tool_name = tool_call["name"]   # 工具名称
                tool_args = tool_call["args"]   # 工具参数
                tool_id = tool_call["id"]       # 工具调用的唯一 ID(关联请求和响应)

                if verbose:
                    print(f"  → 工具:{tool_name},参数:{tool_args}")

                # 执行工具(带错误处理)
                if tool_name not in self.tool_map:
                    tool_result = f"错误:未找到工具 '{tool_name}'"
                else:
                    try:
                        tool_result = self.tool_map[tool_name].invoke(tool_args)
                    except Exception as e:
                        tool_result = f"工具执行错误:{str(e)}"

                if verbose:
                    print(f"     结果:{str(tool_result)[:150]}")

                # ── 步骤D:把工具结果加入消息历史 ──────────────────
                # ToolMessage 必须包含 tool_call_id,与步骤C的 tool_id 对应
                # LLM 通过这个 ID 知道"这个结果是回答哪个工具调用的"
                messages.append(
                    ToolMessage(
                        content=str(tool_result),
                        tool_call_id=tool_id,  # ⚠️ 这个 ID 必须匹配,否则 LLM 会混乱
                    )
                )

            # 步骤E:回到步骤A,LLM 基于新的工具结果继续推理
            if verbose:
                print()

        # 超过最大迭代次数(可能工具一直出错,或任务过于复杂)
        return "已达到最大迭代次数,请尝试更简单的问题。"


# ── 使用示例 ──────────────────────────────────────────────────
@tool
def search_knowledge_base(query: str) -> str:
    """在知识库中搜索相关信息。"""
    knowledge_base = {
        "langchain": "LangChain是一个AI应用开发框架,提供链、工具、Agent等组件,简化LLM应用开发。",
        "langgraph": "LangGraph是基于图状态机的AI工作流框架,支持循环、条件分支和多Agent协作。",
        "python": "Python是一种高级编程语言,以简洁易读著称。1991年由Guido van Rossum创建。",
    }
    for key, value in knowledge_base.items():
        if key.lower() in query.lower():
            return f"【{key}{value}"
    return f"知识库中未找到关于'{query}'的信息"


@tool
def calculate(expression: str) -> str:
    """计算数学表达式,支持基本四则运算和幂运算。"""
    try:
        result = eval(expression, {"__builtins__": {}})
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算错误:{str(e)}"


def demo_agent_loop(llm):
    """演示手动 Agent 循环。"""
    tools = [search_knowledge_base, calculate]
    agent = ManualAgent(llm=llm, tools=tools, max_iterations=10)

    system_prompt = "你是一个知识渊博的AI助手,可以搜索知识库和进行计算。回答时要全面、准确。"

    # 任务1:单一工具
    print("=" * 60)
    print("测试:综合任务(查询 + 计算)")
    print("=" * 60)
    answer = agent.run(
        "Python有多少年历史了(2025年算,Python 1991年创建)?另外,LangGraph是什么?",
        system_prompt=system_prompt,
    )
    print(f"\n最终答案:\n{answer}")

理解 Agent 循环的关键:消息历史就是 Agent 的"记忆"

初始消息:[HumanMessage("问题")]
                    ↓ LLM 推理
第1轮后:[HumanMessage, AIMessage(tool_calls=[calculator])]
                    ↓ 执行工具
          [HumanMessage, AIMessage(tool_calls=[calculator]), ToolMessage("结果")]
                    ↓ LLM 推理(看到了工具结果)
第2轮后:[..., AIMessage("最终答案")]  ← 无 tool_calls,任务完成

五、Step 3:多工具 Agent 与复杂任务

代码文件:lessons/05_agents/03_multi_tool_agent.py

真实 Agent 需要处理多种类型的任务,这要求工具设计合理、分工明确:

import math
import os
import sys
from datetime import datetime, timedelta

from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent  # LangGraph V1.0 起推荐从 langchain.agents 导入
from pydantic import BaseModel, Field


# ── 数学工具组 ──────────────────────────────────────────────────
@tool
def basic_calculator(expression: str) -> str:
    """基础计算器:支持 +、-、*、/、** 运算和括号。"""
    try:
        safe_globals = {"__builtins__": {}, "abs": abs, "round": round, "pow": pow}
        result = eval(expression, safe_globals)
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算错误:{str(e)}"


@tool
def statistics_calculator(numbers: str) -> str:
    """
    统计计算器:计算一组数字的平均值、最大值、最小值、总和。
    数字用逗号分隔,例如:'1, 2, 3, 4, 5'
    """
    try:
        nums = [float(n.strip()) for n in numbers.split(",")]
        return (
            f"数据:{nums}\n"
            f"总和:{sum(nums)}\n"
            f"平均值:{sum(nums)/len(nums):.4f}\n"
            f"最大值:{max(nums)}\n"
            f"最小值:{min(nums)}"
        )
    except ValueError as e:
        return f"数字解析错误:{str(e)}"


# ── 时间工具组 ──────────────────────────────────────────────────
@tool
def get_current_time() -> str:
    """获取当前日期和时间,包括星期几和当年第几天。"""
    now = datetime.now()
    weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
    return (
        f"当前时间:{now.strftime('%Y-%m-%d %H:%M:%S')}\n"
        f"星期:{weekdays[now.weekday()]}\n"
        f"今天是今年第 {now.timetuple().tm_yday} 天"
    )


class DateCalcInput(BaseModel):
    start_date: str = Field(description="起始日期(格式:YYYY-MM-DD,如 2024-01-01)")
    end_date: str = Field(description="结束日期(格式:YYYY-MM-DD,如 2024-12-31)")


@tool(args_schema=DateCalcInput)
def date_calculator(start_date: str, end_date: str) -> str:
    """计算两个日期之间的天数差。"""
    try:
        start = datetime.strptime(start_date, "%Y-%m-%d")
        end = datetime.strptime(end_date, "%Y-%m-%d")
        days = abs((end - start).days)
        return (
            f"从 {start_date}{end_date}:\n"
            f"  相差 {days} 天\n"
            f"  约 {days//7}{days%7} 天\n"
            f"  约 {days/30:.1f} 个月"
        )
    except ValueError as e:
        return f"日期格式错误:{str(e)}"


# ── 展示工具调用过程 ──────────────────────────────────────────────
def run_agent_task(agent, task: str):
    """运行 Agent 任务并展示推理过程。"""
    print(f"\n{'='*60}")
    print(f"任务:{task}")
    print("=" * 60)

    result = agent.invoke({"messages": [HumanMessage(content=task)]})

    # 展示每一步的工具调用(让你看清 Agent 在做什么)
    for msg in result["messages"]:
        msg_type = msg.__class__.__name__
        if msg_type == "AIMessage":
            if msg.tool_calls:
                for tc in msg.tool_calls:
                    print(f"  🔧 调用工具:{tc['name']},参数:{tc['args']}")
            else:
                print(f"\n✅ 最终答案:{msg.content}")
        elif msg_type == "ToolMessage":
            print(f"     └─ 结果:{msg.content[:100]}")


def main():
    api_key = os.getenv("DASHSCOPE_API_KEY")
    if not api_key:
        print("错误:请设置 DASHSCOPE_API_KEY")
        sys.exit(1)

    llm = ChatOpenAI(
        model="qwen-plus",
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
        api_key=api_key,
    )

    # 注册所有工具
    tools = [basic_calculator, statistics_calculator, get_current_time, date_calculator]
    print(f"多工具 Agent 初始化完成,可用工具:{[t.name for t in tools]}\n")

    # create_agent 自动管理所有工具路由(LangGraph V1.0 起推荐使用 create_agent)
    agent = create_agent(llm, tools=tools)

    # 任务1:单工具路由
    run_agent_task(agent, "计算一个半径为5的圆的面积(π×r²),保留两位小数。")

    # 任务2:多工具协作(先查时间,再计算日期差)
    run_agent_task(
        agent,
        "今天是几号?请计算从2024年1月1日到今天共经过了多少天?然后除以7,算出大约几周。"
    )

    # 任务3:统计分析
    run_agent_task(
        agent,
        "计算以下成绩的平均分和最高分:85, 92, 78, 96, 88, 75, 91"
    )


if __name__ == "__main__":
    main()

六、开发 Agent 的完整流程

无论开发什么 Agent,都遵循这 5 个步骤:

flowchart LR
    S1[1. 分析需求\n确定任务边界] --> S2[2. 设计工具\n每个工具一个职责]
    S2 --> S3[3. 单独测试工具\n不经过 LLM 验证]
    S3 --> S4[4. 组装 Agent\ncreate_agent]
    S4 --> S5[5. 端到端测试\n验证推理链路]
    S5 -->|发现问题| S2

步骤1:分析需求,确认是否需要 Agent

# 自问:这个任务的步骤是动态的吗?
# 示例:用户输入"帮我分析这段文字,算出词频最高的5个词,然后生成词云描述"

# 步骤不确定:可能需要先分析、再计算、再生成
# 需要工具:文本分析、计数计算
# → 适合 Agent!

步骤2:设计工具(每个工具只做一件事)

# ✅ 好的工具设计:职责单一,描述清晰
@tool
def count_word_frequency(text: str) -> str:
    """
    统计文本中每个词出现的频率。
    返回按频率排序的词频列表。
    适用于:需要分析文本词汇分布时。
    """
    # ...

# ❌ 不好的工具设计:职责混乱,描述模糊
@tool
def process_text(text: str, mode: str) -> str:
    """处理文本。mode 可以是多种类型。"""  # LLM 不知道什么时候用这个工具
    # ...

步骤3:单独测试工具(不经过 LLM)

# 先直接调用工具,确认功能正确
# 这一步不需要任何 API 费用!

# 测试 calculator
result = calculator.invoke({"expression": "sqrt(144)"})
print(result)  # 期望:12.0

# 测试 get_current_datetime
result = get_current_datetime.invoke({})
print(result)  # 期望:当前日期时间

# 检查工具 Schema(这是 LLM 看到的描述)
import json
print(json.dumps(calculator.args_schema.model_json_schema(), indent=2, ensure_ascii=False))

步骤4:组装 Agent 并测试推理

# 最简单的 Agent 组装
# 最简单的 Agent 组装(LangGraph V1.0 起使用 create_agent)
agent = create_agent(llm, tools=tools)

# 先测试简单问题(确认基础路由正确)
result = agent.invoke({"messages": [HumanMessage(content="计算 2 + 2")]})
print(result["messages"][-1].content)

# 再测试多步骤问题
result = agent.invoke({"messages": [HumanMessage(content="现在几点?再算一下 sqrt(169)")]})

步骤5:生产化改造

from langgraph.checkpoint.memory import MemorySaver

# 添加记忆(多轮对话)
memory = MemorySaver()
agent_with_memory = create_agent(
    llm,
    tools=tools,
    checkpointer=memory,      # 自动保存对话历史
    state_modifier="""你是专业的 AI 助手。
    规则:
    1. 所有计算必须使用工具,不得凭空给出数字
    2. 如果工具出错,告知用户并解释原因
    3. 用简洁的中文回答
    """
)

# 使用 thread_id 区分不同用户的会话
config = {"configurable": {"thread_id": "user_001"}}

def chat_with_memory(message: str) -> str:
    """支持多轮对话的 Agent 调用。"""
    result = agent_with_memory.invoke(
        {"messages": [HumanMessage(content=message)]},
        config=config,
    )
    return result["messages"][-1].content

# 多轮对话测试
print(chat_with_memory("我叫张三,我的生日是 1990-05-15"))
print(chat_with_memory("今天距离我的下一个生日还有几天?"))  # Agent 记住了生日
print(chat_with_memory("我叫什么名字?"))  # Agent 记住了姓名

七、Agent 调试与验证指南

7.1 验证工具是否被正确调用

# 方法1:查看完整消息历史
def debug_agent_run(agent, question: str):
    """调试模式:打印完整的推理链路。"""
    result = agent.invoke({"messages": [HumanMessage(content=question)]})
    messages = result["messages"]

    print(f"📊 Agent 执行统计:")
    print(f"  总消息数:{len(messages)}")
    
    tool_calls = sum(1 for m in messages 
                     if m.__class__.__name__ == "AIMessage" and m.tool_calls)
    print(f"  工具调用轮次:{tool_calls}")

    print("\n📋 完整推理链路:")
    for i, msg in enumerate(messages):
        class_name = msg.__class__.__name__
        if class_name == "HumanMessage":
            print(f"  [{i+1}] 👤 用户:{msg.content}")
        elif class_name == "AIMessage":
            if msg.tool_calls:
                for tc in msg.tool_calls:
                    print(f"  [{i+1}] 🤖 AI决策:调用 {tc['name']},参数:{tc['args']}")
            elif msg.content:
                print(f"  [{i+1}] 🤖 AI回答:{msg.content}")
        elif class_name == "ToolMessage":
            print(f"  [{i+1}] 🔧 工具结果:{msg.content[:100]}")

    return result["messages"][-1].content


# 方法2:检查工具 Schema(验证 LLM 是否能理解工具)
import json
for t in tools:
    schema = t.args_schema.model_json_schema() if t.args_schema else {}
    print(f"\n工具:{t.name}")
    print(f"描述:{t.description[:100]}")
    print(f"参数:{json.dumps(schema.get('properties', {}), ensure_ascii=False)}")

7.2 验证清单

运行 Agent 后,逐项检查:

  • 工具正确选择:Agent 是否选择了预期的工具?(不是瞎猜一个)
  • 参数传递正确:工具接收到的参数格式是否正确?
  • 工具结果被使用:最终答案是否基于工具返回的真实数据?
  • 步骤合理:推理轮数是否合理?(避免过多轮次)
  • 答案符合预期:最终回答是否准确、完整?

7.3 常见错误与解决方案

问题现象根本原因解决方案
Agent 不调用工具直接给出猜测的答案docstring 不够明确,或工具不匹配问题优化工具描述,加"当…时使用此工具"
Agent 死循环一直调用工具不结束工具总是返回错误,LLM 一直重试设置 recursion_limit + 改善工具健壮性
工具参数错误KeyErrorTypeErrorargs_schema 不准确用 Pydantic BaseModel 明确参数类型和描述
多工具时选错用了错误的工具工具 docstring 相似,LLM 混淆让各工具描述的适用场景更有区分度
答案不准确结果不对LLM 没读工具结果,自行推理在 system_prompt 强调"必须基于工具结果"
推理步骤过多耗时长、费用高任务太复杂或工具粒度太细合并相关工具,或拆分大任务

八、课后练习

  1. 天气 Agent:添加真实天气查询工具(调用 OpenWeatherMap API),让 Agent 根据天气给出穿衣建议。
  2. 研究 Agent:实现一个"研究助手",能搜索信息(模拟 Wikipedia)、做数学计算、写结构化摘要。
  3. 调试实践:故意给工具传入错误参数(如 calculator.invoke({"expression": "2 ++ 3"})),观察 Agent 如何处理错误并重试。
  4. 挑战题(多步骤):让 Agent 解决:"如果 A 城市温度是 86°F,B 城市比 A 高 5°C,B 城市温度是多少°F?"(需要温度换算工具和计算工具的配合)
  5. 生产化练习:给 Agent 添加 MemorySaver,实现一个支持5轮以上多轮对话的助手,验证它能记住之前的对话内容。

本章知识点总结

Agent 核心概念
├── ReAct 模式:Thought → Action → Observation → 循环
├── 工具 = @tool 装饰器 + 清晰的 docstring
├── LLM = 大脑(决策),工具 = 手(执行)
└── create_agent = 快速组装,自动管理循环(原名 create_react_agent,LangGraph V1.0 起更名)

vs 其他方案
├── Chain:固定步骤 → Agent:动态决策
├── 纯 LLM:无工具 → Agent:有工具执行能力
└── 纯代码:硬编码路由 → Agent:自然语言路由

开发流程
1. 确认需要 Agent(动态步骤 + 工具 + 自主决策)
2. 设计工具(每工具一职责,docstring 要准确)
3. 单独测试工具(不经过 LLM)
4. 组装 Agent + 系统提示
5. 端到端验证(调试推理链路)

📌 下一章预告:AI 很厉害,但它不知道你私有数据库里有什么。第06章学 RAG(检索增强生成),让 AI 基于你的私有文档回答问题,彻底解决"幻觉"和"知识过时"的问题。

作者:阿聪谈架构
公众号:阿聪谈架构(分享后端架构 / AI / Java 技术文章)
相关代码关注公众号:【阿聪谈架构】 回复:AI专栏代码

本文属于《AI开发入门系列》,后续会持续更新。 关注博主,第一时间收到最新文章,获取完整学习路线与资料