从零构建 AI Agent:基于 LangGraph 的多工具智能体实战(含完整代码)
💡 作者按:RAG 解决了"大模型不懂私有知识"的问题,但企业真正需要的往往不止是问答——自动查天气、订会议室、写周报、调内部 API、执行多步决策,这些都需要 AI Agent。本文带你用 LangGraph 从零搭建一个能自主规划、能调用工具、能处理失败重试的生产级 AI Agent,所有代码均可直接运行。
一、为什么需要 AI Agent?先理解本质
大模型本身是一个"超级大脑",但它有三大局限:
- 没有手和脚——不能主动调用外部系统
- 没有记忆——每次对话都是全新的
- 不会自主规划——你问一步它答一步,不会自己拆解复杂任务
Agent 的核心思想就是给大模型装上三样东西:
┌─────────────────────────────────────────────────────┐ │ AI Agent 架构 │ ├─────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ Planning │───▶│ Tools │───▶│ Memory │ │ │ │ (规划) │ │ (工具调用)│ │ (记忆存储) │ │ │ └─────────┘ └──────────┘ └──────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────┐ │ │ │ LLM (推理引擎) │ │ │ │ 接收输入 → 思考 → 决定动作 → 执行 → 观察 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 最终回答 / 执行结果 │ └─────────────────────────────────────────────────────┘
Agent 的推理循环(ReAct 范式)可以概括为:
思考(Thought)→ 行动(Action)→ 观察(Observation)→ 再思考 → ... → 最终回答
二、技术选型与架构设计
组件 | 选型 | 理由 |
|---|---|---|
Agent 框架 | LangGraph 0.2+ | 相比 LangChain 的 AgentExecutor,LangGraph 提供状态图级别的精细控制,支持循环、条件分支、人工介入 |
LLM | DeepSeek / GPT-4o | 需要较强的推理和工具调用能力 |
工具层 | 自定义 Tool + Tavily Search | 搜索 + 自定义业务工具 |
记忆 | Redis | 跨会话持久化,支持 TTL 过期 |
Web 框架 | FastAPI | 异步、高性能 |
LangGraph vs LangChain AgentExecutor 的核心区别:
LangChain AgentExecutor: 输入 → LLM → 工具 → LLM → 输出(黑盒,难控制) LangGraph: 定义状态图 → 节点 = 函数 → 边 = 条件 → 完全可控
LangGraph 让你能精确控制 Agent 的每一步行为——什么时候调用工具、什么时候该停下来问人、什么时候该重试。
三、环境准备
mkdir ai-agent-langgraph && cd ai-agent-langgraph python -m venv .venvsource .venv/bin/activate pip install fastapi==0.115.0 uvicorn==0.30.0 pip install langgraph==0.2.0 langchain==0.3.0 langchain-core==0.3.0 pip install langchain-openai==0.2.0 pip install tavily-python==0.3.0 pip install redis==5.0.0 python-dotenv==1.0.0 pip install pydantic==2.9.0
.env 文件:# .envLLM_API_KEY=sk-your-key LLM_BASE_URL=https://api.deepseek.com/v1 LLM_MODEL=deepseek-chat TAVILY_API_KEY=tvly-your-tavily-key REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0 MAX_ITERATIONS=10 # Agent 最大推理步数(防无限循环)
💡 Tavily 是专为 AI Agent 设计的搜索 API,比直接用 Bing/Google 更适合 Agent 场景(返回结构化的、去广告的搜索结果)。免费额度足够开发测试。
四、核心代码实现
4.1 配置与客户端
config.py:import osfrom dotenv import load_dotenvfrom openai import OpenAIimport redis
load_dotenv()# LLMLLM_API_KEY = os.getenv("LLM_API_KEY", "")
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")
LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o")# Tavily SearchTAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "")# RedisREDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB = int(os.getenv("REDIS_DB", 0))
MAX_ITERATIONS = int(os.getenv("MAX_ITERATIONS", 10))# 全局客户端llm_client = OpenAI(api_key=LLM_API_KEY, base_url=LLM_BASE_URL)
redis_client = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True)4.2 工具定义(Tools)
tools.py:import requestsfrom typing import List, Dict, Anyfrom tavily import TavilyClientfrom config import TAVILY_API_KEY
tavily = TavilyClient(api_key=TAVILY_API_KEY)def search_web(query: str) -> str: """
搜索互联网获取最新信息。
适用于:新闻、实时数据、常识性问题、需要验证的信息。
"""
try:
results = tavily.search(
query=query,
max_results=5,
search_depth="advanced"
) # 格式化搜索结果
formatted = [] for i, r in enumerate(results.get("results", []), 1):
formatted.append( f"[{i}] {r['title']}\n"
f" URL: {r['url']}\n"
f" 摘要: {r.get('content', '')[:300]}..."
) return "\n\n".join(formatted) if formatted else "未找到相关结果"
except Exception as e: return f"搜索失败: {str(e)}"def get_weather(city: str) -> str: """
获取指定城市的当前天气信息。
适用于:用户询问天气、出行建议等场景。
"""
# 使用 wttr.in 免费天气 API(无需 key)
try:
resp = requests.get( f"https://wttr.in/{city}?format=j1",
timeout=10
)
data = resp.json()
current = data["current_condition"][0]
desc = current["weatherDesc"][0]["value"]
temp_c = current["temp_C"]
feels_like = current["FeelsLikeC"]
humidity = current["humidity"]
wind_kmph = current["windspeedKmph"]
return ( f"{city}当前天气:{desc},气温 {temp_c}°C,"
f"体感 {feels_like}°C,湿度 {humidity}%,"
f"风速 {wind_kmph} km/h"
) except Exception as e: return f"获取天气失败: {str(e)}"def calculate(expression: str) -> str: """
计算数学表达式。支持 + - * / 以及括号。
适用于:数学计算、单位换算等。
"""
try: # 安全计算:只允许数学运算
allowed_chars = set("0123456789+-*/.() ") if not all(c in allowed_chars for c in expression): return "表达式包含非法字符,仅支持数字和 + - * / ()"
result = eval(expression, {"__builtins__": {}}, {}) return f"{expression} = {result}"
except Exception as e: return f"计算失败: {str(e)}"def save_note(title: str, content: str) -> str: """
保存一条笔记到知识库。
适用于:用户要求记录信息、写备忘、保存重要内容。
"""
from config import redis_client import json import time
note_id = f"note:{int(time.time())}"
note = { "title": title, "content": content, "created_at": time.strftime("%Y-%m-%d %H:%M:%S")
}
redis_client.set(note_id, json.dumps(note, ensure_ascii=False))
redis_client.expire(note_id, 7 * 24 * 3600) # 7天过期
return f"笔记已保存:{title}(ID: {note_id})"# 工具注册表:Agent 能看到的所有工具TOOLS = { "search_web": { "func": search_web, "description": "搜索互联网获取最新信息。输入:搜索关键词(字符串)", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}
}, "required": ["query"]
}
}, "get_weather": { "func": get_weather, "description": "获取指定城市的当前天气。输入:城市名称(字符串,如'北京'、'London')", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"}
}, "required": ["city"]
}
}, "calculate": { "func": calculate, "description": "计算数学表达式。输入:表达式字符串(如 '123 * 456')", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式"}
}, "required": ["expression"]
}
}, "save_note": { "func": save_note, "description": "保存笔记。输入:标题和内容", "parameters": { "type": "object", "properties": { "title": {"type": "string", "description": "笔记标题"}, "content": {"type": "string", "description": "笔记内容"}
}, "required": ["title", "content"]
}
}
}4.3 LangGraph 状态图定义
agent_graph.py:import jsonfrom typing import TypedDict, List, Optional, Annotatedfrom langgraph.graph import StateGraph, ENDfrom langgraph.prebuilt import ToolNodefrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom config import llm_client, LLM_MODEL, MAX_ITERATIONSfrom tools import TOOLS# ========== 状态定义 ==========class AgentState(TypedDict): """Agent 的完整状态,在图的节点间传递"""
messages: List[dict] # 对话历史
iterations: int # 当前推理步数
max_iterations: int # 最大步数限制
final_answer: Optional[str] # 最终答案(有值则结束)# ========== 系统提示词 ==========SYSTEM_PROMPT = """你是一个智能助手,可以调用工具来完成任务。
## 可用工具
{tools_description}
## 工作规则
1. 分析用户需求,决定是否需要调用工具
2. 如果需要工具,输出 JSON 格式:{{"tool": "工具名", "args": {{"参数名": "参数值"}}}}
3. 如果工具返回了结果,分析结果并决定下一步(继续调用工具 or 给出最终答案)
4. 如果信息已足够回答用户,输出最终答案(纯文本,不要 JSON)
## 重要
- 每次只调用一个工具
- 工具调用结果会在下一步以"Observation"形式提供给你
- 最终答案要简洁、准确、有结构
"""def format_tools_description() -> str: """格式化工具描述,注入到系统提示词"""
descriptions = [] for name, info in TOOLS.items():
descriptions.append(f"- **{name}**: {info['description']}") return "\n".join(descriptions)# ========== 节点函数 ==========def agent_node(state: AgentState) -> AgentState: """
Agent 推理节点:接收当前状态,决定下一步动作。
这是 Agent 的"大脑"——思考、规划、决策。
"""
messages = state["messages"]
iterations = state.get("iterations", 0)
# 步数安全检查
if iterations >= state.get("max_iterations", MAX_ITERATIONS): return {
**state, "final_answer": "抱歉,任务过于复杂,已达到最大推理步数限制。请尝试简化问题。", "iterations": iterations
}
# 构造消息列表
system_msg = SystemMessage(
content=SYSTEM_PROMPT.format(tools_description=format_tools_description())
)
# 转换历史消息为 LangChain 格式
lc_messages = [system_msg] for msg in messages: if msg["role"] == "user":
lc_messages.append(HumanMessage(content=msg["content"])) elif msg["role"] == "assistant":
lc_messages.append(AIMessage(content=msg["content"])) elif msg["role"] == "observation": # 工具返回结果作为 observation 注入
lc_messages.append(HumanMessage(
content=f"Observation: {msg['content']}"
))
# 调用 LLM
resp = llm_client.chat.completions.create(
model=LLM_MODEL,
messages=[
{"role": "system", "content": system_msg.content},
*[{"role": m.type, "content": m.content} for m in lc_messages[1:]]
],
temperature=0.1,
response_format={"type": "text"}
)
content = resp.choices[0].message.content.strip()
# 判断是工具调用还是最终答案
try: # 尝试解析为 JSON(工具调用)
parsed = json.loads(content) if "tool" in parsed and "args" in parsed:
tool_name = parsed["tool"]
tool_args = parsed["args"]
if tool_name not in TOOLS: # 工具不存在,告诉 Agent
new_messages = messages + [
{"role": "assistant", "content": content},
{"role": "observation", "content": f"错误:工具 '{tool_name}' 不存在"}
] else: # 执行工具
tool_func = TOOLS[tool_name]["func"] try:
result = tool_func(**tool_args) except Exception as e:
result = f"工具执行失败: {str(e)}"
new_messages = messages + [
{"role": "assistant", "content": content},
{"role": "observation", "content": result}
]
return {
**state, "messages": new_messages, "iterations": iterations + 1, "final_answer": None
} except json.JSONDecodeError: pass # 不是 JSON,当作最终答案
# 最终答案
new_messages = messages + [{"role": "assistant", "content": content}] return {
**state, "messages": new_messages, "iterations": iterations + 1, "final_answer": content
}def should_continue(state: AgentState) -> str: """
条件边:判断是继续推理还是结束。
"""
if state.get("final_answer"): return "end"
return "continue"# ========== 构建图 ==========def build_graph(): """构建 LangGraph 状态图"""
graph = StateGraph(AgentState)
# 添加节点
graph.add_node("agent", agent_node)
# 设置入口
graph.set_entry_point("agent")
# 添加条件边
graph.add_conditional_edges( "agent",
should_continue,
{ "continue": "agent", # 循环回 agent 节点(继续推理)
"end": END # 结束
}
)
return graph.compile()4.4 FastAPI 服务
main.py:import uuidfrom typing import List, Optionalfrom fastapi import FastAPI, HTTPExceptionfrom fastapi.middleware.cors import CORSMiddlewarefrom pydantic import BaseModelimport loggingfrom config import redis_clientfrom agent_graph import build_graph
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="AI Agent 服务",
description="基于 LangGraph 的多工具智能体 API",
version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"]
)# 编译图(全局单例)graph = build_graph()# ========== 数据模型 ==========class ChatRequest(BaseModel):
message: str
session_id: Optional[str] = Noneclass ChatResponse(BaseModel):
answer: str
session_id: str
iterations: int
tool_calls: List[dict]# ========== 会话管理 ==========def get_session(session_id: str) -> dict: """从 Redis 获取会话状态"""
data = redis_client.get(f"session:{session_id}") if data: import json return json.loads(data) return {"messages": []}def save_session(session_id: str, state: dict): """保存会话状态到 Redis(30分钟过期)"""
import json
redis_client.setex( f"session:{session_id}", 1800,
json.dumps(state, ensure_ascii=False)
)# ========== API 路由 ==========@app.post("/chat", response_model=ChatResponse)async def chat(req: ChatRequest): """
与 Agent 对话。
支持多轮对话:传入 session_id 可保持上下文。
"""
session_id = req.session_id or str(uuid.uuid4())
# 获取历史会话
session = get_session(session_id)
# 添加用户消息
session["messages"].append({ "role": "user", "content": req.message
})
# 初始化状态
initial_state = { "messages": session["messages"], "iterations": 0, "max_iterations": 10, "final_answer": None
}
try: # 执行图
final_state = graph.invoke(initial_state)
# 提取最终答案
answer = final_state.get("final_answer", "抱歉,未能生成答案。")
iterations = final_state.get("iterations", 0)
# 提取工具调用记录
tool_calls = []
messages = final_state.get("messages", []) for msg in messages: if msg.get("role") == "assistant": try: import json
parsed = json.loads(msg["content"]) if "tool" in parsed:
tool_calls.append(parsed) except (json.JSONDecodeError, KeyError): pass
# 更新会话(只保留最近 20 轮)
updated_messages = final_state.get("messages", [])
session["messages"] = updated_messages[-40:] # 每轮 user+assistant = 2条
save_session(session_id, session)
logger.info(f"Session {session_id}: {iterations} iterations, {len(tool_calls)} tool calls")
return ChatResponse(
answer=answer,
session_id=session_id,
iterations=iterations,
tool_calls=tool_calls
)
except Exception as e:
logger.error(f"Agent 执行失败: {e}") raise HTTPException(status_code=500, detail=f"Agent 执行异常: {str(e)}")@app.get("/sessions/{session_id}")async def get_session_info(session_id: str): """查看会话详情"""
session = get_session(session_id) return { "session_id": session_id, "message_count": len(session.get("messages", [])), "messages": session.get("messages", [])
}@app.delete("/sessions/{session_id}")async def clear_session(session_id: str): """清除会话"""
redis_client.delete(f"session:{session_id}") return {"status": "cleared", "session_id": session_id}@app.get("/health")async def health(): return {"status": "healthy", "service": "ai-agent"}# ========== 启动 ==========if __name__ == "__main__": import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)五、运行与测试
5.1 启动服务
# 确保 Redis 已启动redis-server# 启动 Agent 服务uvicorn main:app --host 0.0.0.0 --port 8001 --reload
访问
http://localhost:8001/docs 查看 Swagger 文档。5.2 测试对话
# 1. 简单计算 + 天气查询(多步推理)curl -X POST "http://localhost:8001/chat" \
-H "Content-Type: application/json" \
-d '{
"message": "北京今天天气怎么样?另外帮我算一下 15 * 28 是多少"
}'# 返回示例:# {# "answer": "北京今天天气:晴,气温 22°C,体感 21°C,湿度 45%,风速 12 km/h。\n\n另外,15 × 28 = 420。",# "session_id": "a1b2c3d4-...",# "iterations": 3,# "tool_calls": [# {"tool": "get_weather", "args": {"city": "北京"}},# {"tool": "calculate", "args": {"expression": "15 * 28"}}# ]# }# 2. 多轮对话(带上下文)curl -X POST "http://localhost:8001/chat" \
-H "Content-Type: application/json" \
-d '{
"message": "帮我记一下,今天下午3点要开会讨论Q4规划",
"session_id": "a1b2c3d4-..."
}'# 3. 搜索 + 总结curl -X POST "http://localhost:8001/chat" \
-H "Content-Type: application/json" \
-d '{
"message": "帮我搜索一下 LangGraph 和 LangChain 的最新区别,用中文总结"
}'5.3 Agent 推理过程可视化
Agent 处理上述第一个请求时的内部推理链:
Step 1: 用户 → "北京今天天气怎么样?另外帮我算一下 15 * 28"
Agent 思考 → 需要调用两个工具
Agent 行动 → {"tool": "get_weather", "args": {"city": "北京"}}
Step 2: Observation → "北京当前天气:晴,气温 22°C..."
Agent 思考 → 天气已获取,还需要计算
Agent 行动 → {"tool": "calculate", "args": {"expression": "15 * 28"}}
Step 3: Observation → "15 * 28 = 420"
Agent 思考 → 两个任务都完成了,可以给出最终答案
Agent 回答 → "北京今天天气:晴... 另外,15 × 28 = 420。"六、生产级增强方案
6.1 流式输出(SSE)
用户不希望等 Agent 推理完才看到结果,应该实时看到每一步:
from sse_starlette.sse import EventSourceResponseimport asyncio@app.post("/chat/stream")async def chat_stream(req: ChatRequest): """流式输出 Agent 推理过程"""
async def event_generator(): # 逐步 yield 每一步推理
yield {"event": "thinking", "data": "正在分析您的问题..."}
# 执行图,每步 yield
async for step in graph.astream(initial_state): if step.get("tool_call"): yield { "event": "tool_call", "data": json.dumps(step["tool_call"])
} if step.get("observation"): yield { "event": "observation", "data": step["observation"]
}
yield { "event": "final_answer", "data": final_state["final_answer"]
}
return EventSourceResponse(event_generator())6.2 人工介入(Human-in-the-Loop)
某些操作(如发送邮件、删除数据)需要人工确认:
def human_approval_node(state: AgentState) -> AgentState: """人工审批节点"""
pending_action = state.get("pending_action") # 暂停图执行,等待人工输入
# LangGraph 的 interrupt() 机制
...# 在图中添加审批边graph.add_node("human_approval", human_approval_node)
graph.add_edge("agent", "human_approval")
graph.add_conditional_edges("human_approval", check_approval)6.3 工具调用失败自动重试
def agent_node_with_retry(state: AgentState) -> AgentState: """带重试的 Agent 节点"""
max_retries = 2
for attempt in range(max_retries + 1): try: return agent_node(state) except Exception as e: if attempt == max_retries:
state["final_answer"] = f"执行失败,已重试 {max_retries} 次: {str(e)}"
return state
state["messages"].append({ "role": "observation", "content": f"第 {attempt+1} 次尝试失败: {e},正在重试..."
})6.4 多 Agent 协作
复杂场景可以用"主管 Agent + 专家 Agent"模式:
┌─────────────────────────────────────────┐ │ Supervisor Agent │ │ (理解意图,分配子任务) │ ├──────────┬──────────┬───────────────────┤ │ 搜索Agent │ 代码Agent │ 数据Agent │ │ (Tavily) │ (Python) │ (SQL/API) │ └──────────┴──────────┴───────────────────┘
七、踩坑清单
坑 1:LLM 输出格式不稳定
即使你要求输出 JSON,模型偶尔还是会输出带 markdown 代码块的格式(
json ...)。一定要做格式清洗:import redef extract_json(text: str) -> dict: # 尝试直接解析
try: return json.loads(text) except json.JSONDecodeError: pass
# 尝试提取代码块
match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text) if match: return json.loads(match.group(1)) raise ValueError(f"无法解析 JSON: {text[:100]}")坑 2:工具描述写不好,Agent 就不会用工具
工具描述是 Agent 决定"何时用、怎么用"的唯一依据。描述要:
- ✅ 明确说明适用场景("当用户询问天气时使用")
- ✅ 给出参数示例("city: '北京' 或 'London'")
- ❌ 不要写模糊描述("获取一些信息")
坑 3:无限循环
Agent 可能陷入"调用工具 → 结果不满意 → 换个参数再调 → 还是不满意"的死循环。必须设置
max_iterations,并在接近上限时给 Agent 一个"强制总结"的提示。坑 4:上下文窗口爆炸
多轮对话中,历史消息会不断累积。如果 Agent 每轮都把所有历史塞给 LLM,很快会超出上下文窗口。策略:
- 只保留最近 N 轮
- 对旧消息做摘要压缩
- 工具调用的中间结果不进入长期记忆
坑 5:并发安全问题
LangGraph 的
StateGraph 本身是线程安全的,但如果你在节点函数中使用了全局可变状态(如共享的计数器),需要加锁。八、项目结构
ai-agent-langgraph/ ├── config.py # 配置与客户端 ├── tools.py # 工具定义与注册 ├── agent_graph.py # LangGraph 状态图 ├── main.py # FastAPI 入口 ├── requirements.txt ├── .env └── tests/ ├── test_tools.py # 工具单元测试 └── test_agent.py # Agent 集成测试
九、总结
本文实现的 AI Agent 具备以下生产级特性:
- ✅ 多工具编排:搜索、天气、计算、笔记,可无限扩展
- ✅ 自主规划:Agent 自己决定调用哪些工具、调用顺序
- ✅ 循环推理:观察 → 思考 → 行动,直到任务完成
- ✅ 会话记忆:Redis 持久化,支持多轮对话
- ✅ 安全控制:步数限制、工具白名单、输入校验
- ✅ 可观测:每步工具调用都有记录,方便调试
RAG vs Agent 怎么选?
场景 | 选 RAG | 选 Agent |
|---|---|---|
问答、知识检索 | ✅ | |
需要实时数据 | ✅ | |
需要执行操作 | ✅ | |
多步复杂任务 | ✅ | |
简单信息查找 | ✅ |
下一步演进:
- MCP 协议接入:用 Model Context Protocol 标准化工具定义,让 Agent 能接入任意第三方服务
- 多 Agent 编排:用 LangGraph 的
SendAPI 实现动态子图分发 - RAG + Agent 融合:Agent 自己决定什么时候该检索知识库
- 评估体系:用 LangSmith 追踪每次推理链路,量化 Agent 表现
📌 老架构师的话:Agent 框架再花哨,核心还是 Prompt 质量 + 工具设计 + 状态管理。框架只是帮你把 ReAct 循环工程化,真正的智能来自于你对业务场景的理解和工具链的精心设计。别被"Agent"这个词唬住——它本质上就是一个while循环里套了个 LLM 调用,关键在于你怎么设计循环里的每一步。
