AI 全栈学习之旅 -Week 9:什么是AI Agent?从Function Calling到LangGraph

什么是AI Agent?从Function Calling到LangGraph

适合读者:有Python基础、用过前端框架、想搞懂AI Agent到底怎么"调用工具"的人。

本文目标:让你亲手跑通一个完整的AI Agent,从最底层的Function Calling原理,到手写ReAct循环,再到用LangGraph优雅实现,最后加上记忆持久化和错误处理。


一、先破除一个误解:LLM并不会真的"调用"你的函数

很多初学者第一次接触Agent都会困惑:

"我把Python函数交给大模型,它就能自己执行?"

答案是:不会。LLM本质上还是一个文本生成模型。所谓的Function Calling / Tool Use,其实是这样一个流程:

  1. 你告诉LLM:我有这些工具,它们的名字、参数格式、作用是什么(以JSON Schema的形式)。

  2. 用户提问,LLM判断"这个问题需要借助工具才能回答"。

  3. LLM不直接执行函数,而是返回一段结构化的指令,例如:

    json 复制代码
    json
    json
    {"name": "get_weather", "args": {"city": "北京"}}
  4. 你的Python代码解析这段指令 → 真正去调用函数 → 把结果再塞回对话历史。

  5. LLM拿到结果后继续推理,直到能回答用户的问题为止。

前端类比

LLM就像前端页面,工具就像后端的API接口。tool_calls不是"已经调用完了",而是"前端决定要发请求"。真正发起请求、拿到响应、再更新状态(setState)的是你的Agent运行时代码。


二、用 @tool + bind_tools 定义工具(Function Calling入门)

我们先从最简单的开始:定义两个工具(计算器和查天气),然后让LLM学会"调用"它们。

安装依赖

复制代码
bash
bash
pip install langchain-openai langchain-core langgraph langgraph-checkpoint-sqlite python-dotenv

配置 .env 文件

ini 复制代码
env
env
DEEPSEEK_API_KEY=sk-你的key
MODEL_NAME=deepseek-v4-flash
DEEPSEEK_BASE_URL=https://api.deepseek.com

最小可运行示例

python 复制代码
python
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

load_dotenv()

# 初始化LLM(使用DeepSeek,兼容OpenAI接口)
llm = ChatOpenAI(
    model=os.getenv("MODEL_NAME", "deepseek-v4-flash"),
    temperature=0,
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
)

# 定义两个工具
@tool
def calculator(a: int, b: int) -> int:
    """计算两个整数相乘的结果。"""
    return a * b

@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气情况。"""
    weather_db = {
        "北京": "晴,25度",
        "上海": "多云,28度",
        "广州": "雷阵雨,30度"
    }
    return f"{city} 天气:{weather_db.get(city, '未知城市')}"

tools = [calculator, get_weather]
llm_with_tools = llm.bind_tools(tools)

# 测试:同时问两个问题
msg = llm_with_tools.invoke([HumanMessage(content="北京天气怎么样?再算12乘以34")])
print("LLM回复内容:", msg.content)
print("工具调用请求:", msg.tool_calls)

输出示例

css 复制代码
纯文本
纯文本
LLM回复内容: 
工具调用请求: [    {'name': 'get_weather', 'args': {'city': '北京'}, 'id': 'call_abc123', 'type': 'tool_call'},    {'name': 'calculator', 'args': {'a': 12, 'b': 34}, 'id': 'call_def456', 'type': 'tool_call'}]

关键点

  • @tool 装饰器会自动把函数名、参数类型、docstring转换成JSON Schema,发送给LLM。
  • bind_tools 并不是"注册回调函数",而是把工具的描述信息附加到每次LLM请求的prompt中。
  • LLM返回的 tool_calls 只是一个"请求列表",真正的工具执行还需要我们自己完成。

三、手写ReAct循环:不依赖框架,看清Agent的本质

ReAct = Reason(推理) + Act(行动)。

核心就是一个循环:Thought → Action → Observation → 再Thought

手写版ReAct

python 复制代码
python
python
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage

# 建立工具名称到函数的映射
tool_map = {t.name: t for t in tools}

def react(question: str, max_rounds: int = 5):
    messages = [HumanMessage(content=question)]

    for i in range(max_rounds):
        print(f"\n=== 第 {i+1} 轮 ===")
        ai_msg: AIMessage = llm_with_tools.invoke(messages)
        messages.append(ai_msg)

        # 如果没有工具调用,说明LLM已经给出了最终答案
        if not ai_msg.tool_calls:
            return ai_msg.content

        # 逐个执行工具
        for call in ai_msg.tool_calls:
            name = call["name"]
            args = call["args"]
            call_id = call["id"]
            result = tool_map[name].invoke(args)
            print(f"执行工具 {name}({args}) => {result}")
            # 将工具结果包装成ToolMessage,并关联对应的tool_call_id
            messages.append(ToolMessage(content=str(result), tool_call_id=call_id))

    return "超过最大轮数,未得出答案"

# 测试
print(react("北京天气怎么样?再算12乘以34"))

运行过程

dart 复制代码
纯文本
纯文本
=== 第 1 轮 ===
执行工具 get_weather({'city': '北京'}) => 北京 天气:晴,25度
执行工具 calculator({'a': 12, 'b': 34}) => 408

=== 第 2 轮 ===
LLM回复:北京天气晴朗,25度;12乘以34等于408。

核心要点

  1. 消息历史必须完整:每次调用LLM都要带上全部历史消息,包括之前的提问、LLM的回复、工具的执行结果。
  2. ToolMessage 必须携带 tool_call_id :这个ID必须和前面 AIMessage.tool_calls 中的 id 一一对应,否则LLM会报错"tool消息没有对应的tool_calls"。
  3. 熔断机制max_rounds 限制最大循环次数,防止LLM陷入死循环(比如一直调用工具而不给出最终答案)。

四、用LangGraph把ReAct写成"图"

手写while循环虽然直观,但工程化时会有不少问题:节点难以复用、条件分支多了代码混乱、想要添加记忆持久化或人工审核会很麻烦。

LangGraph的思路:把"调用LLM"和"执行工具"设计成两个节点,用有向边来控制流程流转。

4.1 定义状态

python 复制代码
python
python
from typing import TypedDict, Annotated, Literal
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

这里的 add_messages 是一个Reducer(归约器),它的作用是:每次节点返回 {"messages": [...]} 时,不是覆盖原有的消息列表,而是把新消息追加进去。

前端类比:就像Redux中的combineReducers,messages这个状态会通过concat的方式更新,而不是整体替换。

4.2 定义节点和条件边

python 复制代码
python
python
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode

def call_model(state: AgentState):
    """LLM推理节点"""
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

tool_node = ToolNode(tools)  # 内置的ToolNode会自动执行工具并返回ToolMessage

def should_continue(state: AgentState) -> Literal["tools", END]:
    """条件判断:是否需要继续调用工具"""
    last_message = state["messages"][-1]
    if isinstance(last_message, AIMessage) and last_message.tool_calls:
        return "tools"
    return END

# 构建图
builder = StateGraph(AgentState)
builder.add_node("agent", call_model)
builder.add_node("tools", tool_node)

builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")

app = builder.compile()

流程图

sql 复制代码
纯文本
纯文本
START → agent → 有tool_calls? → tools → agent → ...
                |-> 无tool_calls → END

4.3 运行测试

css 复制代码
python
python
from langchain_core.messages import HumanMessage

result = app.invoke({"messages": [HumanMessage(content="北京天气怎么样?再算12乘以34")]})
print(result["messages"][-1].content)

输出

复制代码
纯文本
纯文本
北京天气晴朗,25度;12乘以34等于408。

对比手写版

  • 手写版用 while 循环 + if-else 判断;LangGraph用 StateGraph + conditional_edges
  • 手写版需要手动管理消息列表;LangGraph用 add_messages 自动累加。
  • 手写版需要手动执行工具并构造 ToolMessage;LangGraph的 ToolNode 自动完成。

五、增加记忆持久化:SqliteSaver

手写ReAct重启后就丢失了对话历史;LangGraph通过Checkpointer可以将每一步的状态保存到数据库,下次启动时还能恢复。

5.1 使用SqliteSaver

ini 复制代码
python
python
from langgraph.checkpoint.sqlite import SqliteSaver

# 使用with语句(新版LangGraph要求)
with SqliteSaver.from_conn_string("checkpoints.db") as checkpointer:
    checkpointer.setup()  # 初始化数据库表
    app = builder.compile(checkpointer=checkpointer)

    # 第一次对话,会话ID为"session1"
    config = {"configurable": {"thread_id": "session1"}}
    app.invoke(
        {"messages": [HumanMessage(content="我叫小明,帮我算12乘以34")]},
        config
    )

    # 第二次对话,同一会话,Agent应该还记得用户的名字
    result = app.invoke(
        {"messages": [HumanMessage(content="你还记得我叫什么吗?")]},
        config
    )
    print(result["messages"][-1].content)  # 输出:你叫小明呀!

    # 新会话,Agent应该忘记之前的信息
    config2 = {"configurable": {"thread_id": "session2"}}
    result2 = app.invoke(
        {"messages": [HumanMessage(content="你还记得我叫什么吗?")]},
        config2
    )
    print(result2["messages"][-1].content)  # 输出:抱歉,我没有关于您名字的记忆...

关键点

  • thread_id 就是会话标识,不同用户/不同会话用不同的id,互相隔离。
  • 同一个 thread_id 再次invoke时,LangGraph会自动从数据库加载历史消息,无需手动拼接。
  • from_conn_string 在新版LangGraph中是一个上下文管理器(Context Manager),必须用 with 语句包裹,或者在外部用 sqlite3.connect 手动创建连接后传入。
  • 文件型SQLite重启不丢失数据;:memory: 模式重启即空。

5.2 加上错误处理和熔断

在生产环境中,我们还需要:

  • 重试机制 :LLM调用可能因为网络波动失败,设置 max_retries
  • 熔断机制:限制最大推理轮数,防止无限循环。

完整的Agent节点可以这样写:

python 复制代码
python
python
MAX_ITERATIONS = 5  # 最大推理轮数

def call_model(state: AgentState):
    # 统计已有的AI消息数量,判断是否达到上限
    ai_messages = [m for m in state["messages"] if isinstance(m, AIMessage)]
    if len(ai_messages) >= MAX_ITERATIONS:
        return {"messages": [AIMessage(content="❌ 已达到最大推理次数,请简化问题。")]}
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

同时在初始化LLM时加上重试参数:

ini 复制代码
python
python
llm = ChatOpenAI(
    model=os.getenv("MODEL_NAME", "deepseek-v4-flash"),
    temperature=0,
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
    max_retries=3,      # 网络错误时最多重试3次
    timeout=30          # 每次请求超时30秒
)

六、整体对照:从裸LLM到Agent

能力 裸LLM 手写ReAct LangGraph ReAct
调用工具 不支持 tool_calls + 手动执行 ToolNode 自动执行
多轮推理 自己拼历史 while + messages StateGraph 循环边
条件分支 if/else if tool_calls conditional_edges
消息累积 手动append 手动append add_messages reducer
持久记忆 自己存库 自己写文件 SqliteSaver / Postgres
生产扩展 中等 高(可加HITL、重试、观测)

结论

  • 学原理:先手写ReAct。
  • 做项目:用LangGraph手工图;再简单可直接用 create_react_agent(LangGraph预置)。
  • 上生产:checkpointer + 限轮 + 工具超时降级 + 日志追踪一起上。

七、常见踩坑记录(Week 9 实战经验)

  1. **工具返回结果后,模型报错 role='tool' 没有对应 tool_calls**
    → 历史中前一条必须是带有 tool_calls 的AIMessage;使用 ToolMessage(tool_call_id=...) 回传,不要用普通HumanMessage。
  1. LangGraph状态越跑越"失忆"
    → 状态定义中 messages 没有使用 Annotated[list, add_messages],导致节点返回值覆盖了整个消息列表。节点只需返回新增的消息,不要返回全量替换。
  2. **SqliteSaver报错 contextmanagerInvalid checkpoint**
    → 新版LangGraph中 from_conn_string 返回的是一个上下文管理器,必须用 with 语句包裹;或者手动创建 sqlite3.Connection 再实例化 SqliteSaver
  1. DeepSeek不调用工具
    → 确认使用了OpenAI兼容接口 ChatOpenAI(base_url=...);工具docstring写清楚参数含义;temperature 调低(0或接近0);不要传递旧版教程中的 method="function_calling" 参数,新版 bind_tools 按当前langchain-openai默认即可。
  2. 工具结果太大撑爆上下文窗口
    → 天气、搜索等工具只返回核心字段;超长文本截断到1000~2000字符再回传。

八、小结

AI Agent不是"LLM变聪明了自己跑代码",而是:

模型负责决策"用哪个工具、传什么参数";运行时负责执行工具、回传结果;图框架负责把"决策---执行---再决策"这个循环管起来。

最小技术栈:

  • 工具定义:@tool
  • 模型绑定:bind_tools
  • 决策输出:AIMessage.tool_calls
  • 执行回传:ToolNode 或手工 ToolMessage
  • 循环编排:手写 while / LangGraph StateGraph
  • 记忆持久:SqliteSaver(开发)、PostgresSaver(生产)

按这个顺序从Day1跑通Function Calling、Day3手写ReAct、Day4改LangGraph、Day5加SqliteSaver,Agent底层就不虚了。


附录:Week 9 完整代码仓库结构 源码地址

bash 复制代码
纯文本
纯文本
week9/
├── .env                     # API密钥配置
├── week9_day1.py           # Function Calling 原理 + 3个工具定义
├── week9_day2.py           # get_weather 接入 wttr.in 真实API + 多工具并行测试
├── week9_day3.py           # 纯Python手写ReAct循环
├── week9_day4.py           # LangGraph StateGraph版ReAct
├── week9_day5.py           # SqliteSaver记忆持久化 + 重试 + 熔断
└── checkpoints.db           # SQLite持久化文件(运行后自动生成)

希望这篇博客能帮你从零搭建起对AI Agent的理解。如果你有任何疑问或踩了新的坑,欢迎留言交流!

**

相关推荐
工具派1 小时前
markdown在线编辑器怎么选?渲染管线的3个坑和md转PDF跑版记录
前端·后端
平头哥技术团队1 小时前
Day 13 | 调 line-height 和 margin:三处间距让名片页脱离模板感
开发语言·前端·javascript·学习·html5
2601_954811821 小时前
AI科学实验室MHS标准解读:AI智能体如何统一控制实验室设备接口
人工智能·python
用户0332126663671 小时前
使用 Python 为 PowerPoint 设置背景色和背景图【附代码示例】
python
计算机魔术师1 小时前
Claude Opus 5 干不过人类客服?23.9% 的通过率撕开 Agent 真相
前端
ever_up9731 小时前
LangChain基础知识概述1
人工智能·python·langchain
cjy0001112 小时前
2026年9月零基础能听懂国内 FDE 讲师的课吗?
大数据·前端·人工智能·fde
asdzx672 小时前
Python 实现 PDF 文本查找与高亮标注
开发语言·python·pdf
zander2582 小时前
LeetCode 1143. 最长公共子序列
开发语言·python·算法