【Agent 实战】Phase 1:从零搭建工具 Agent(LangGraph + 通义千问 + Function Calling)
摘要:本文记录 Agent 学习路线 Phase 1 的完整实战过程——如何让大模型不再只会「动嘴」,而是学会调用外部工具「动手做事」。从最简单的「感知→决策→行动」原型出发,逐步搭建基于 LangGraph 的工具 Agent,实现天气查询、数学计算、时间查询,并理解 agent ↔ tools 循环、Trace 日志与对话记忆。
关键词:Agent、LangGraph、Function Calling、通义千问、工具调用、ReAct、大模型应用
一、前言:Agent 是什么?
开始学习 Agent 之前,我先问自己一个问题:Agent 和普通 ChatGPT 聊天有什么区别?
普通聊天:你问一句,它答一句,全靠模型「记忆」和「推理」。
Agent:多了一个能力——在循环里做决策,并调用外部工具获取真实数据、执行真实动作。
普通聊天:用户 → LLM → 回答
工具 Agent:用户 → LLM 决策 → 调工具 → 拿到结果 → LLM 汇总 → 回答
一句话:Agent 给 LLM 装上了「手」。
Phase 1 的目标就是搭建这样一双手:让通义千问能查天气、做计算、报时间。
二、从规则 Agent 到 LLM Agent
2.1 最初的原型:test.py
项目里保留了一个最简原型 test.py,用硬编码规则模拟 Agent 三步:
感知(perceive)→ 决策(make_decision)→ 行动(execute_action)
if "天气" in observation:
decision = "调用天气查询工具"
elif "计算" in observation:
decision = "调用计算器工具"
这能帮助理解 Agent 的本质结构,但有两个致命问题:
| 问题 | 说明 |
|---|---|
| 规则写死 | 用户说「帮我看看北京冷不冷」匹配不到「天气」 |
| 无法扩展 | 每加一个工具就要改 if-else |
2.2 升级:让 LLM 来做决策
Phase 1 的核心思路:把「决策模块」从 if-else 换成大模型,通过 Function Calling(工具调用)让 LLM 自己决定:
- 要不要调工具?
- 调哪个工具?
- 传什么参数?
这就是工业级 Agent 的基础模式。
三、工具 Agent 是什么?
3.1 核心概念
| 概念 | 含义 |
|---|---|
| Tool(工具) | 一个 Python 函数,有明确的输入输出 |
| Function Calling | LLM 输出结构化的「工具调用请求」 |
| ToolNode | LangGraph 内置节点,负责执行工具 |
| agent ↔ tools 循环 | LLM 决策 → 执行工具 → 结果回填 → LLM 再决策 |
3.2 工作流架构
用户提问
↓
agent 节点(LLM 决策)
↓
需要调工具?
├─ 是 → tools 节点(执行工具)→ 回到 agent 节点
└─ 否 → 输出最终回答
与 Phase 2 RAG 的对比(系列文章):
| 维度 | Phase 1 工具 Agent | Phase 2 RAG |
|---|---|---|
| 解决什么问题 | 执行实时动作 | 基于私有文档问答 |
| 数据来源 | 外部 API | 本地向量库 |
| 工作流 | agent ↔ tools 循环 | retrieve → generate |
| 典型提问 | 「北京天气怎么样?」 | 「年假有几天?」 |
3.3 ReAct 范式
工具 Agent 背后对应的是 ReAct(Reason + Act) 范式:
Thought(思考)→ Action(行动)→ Observation(观察)→ 循环
在本项目中:
- Thought/Action:agent 节点,LLM 决定调用
get_weather("北京") - Observation:tools 节点返回「北京:晴,25°C」
- 循环:agent 看到结果,决定继续调工具或直接回答
四、项目结构
FirstAgent/
├── agent.py # LangGraph 主图 + 对话循环
├── config.py # 环境变量配置
├── trace.py # Trace 日志
├── tools/
│ ├── weather.py # 天气工具(wttr.in 免费 API)
│ ├── calculator.py # 安全计算器(AST 解析)
│ └── time_tool.py # 当前时间工具(练习添加)
├── test.py # Phase 0 规则 Agent 原型(对照学习)
├── requirements.txt
├── .env # API Key(不要提交 Git)
└── run.ps1 # 一键启动
五、环境准备
5.1 安装依赖
cd FirstAgent
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
主要依赖:
langgraph—— 工作流编排langchain-core/langchain-openai—— LLM 与工具抽象openai—— 通义千问兼容 OpenAI SDKpython-dotenv—— 环境变量管理requests—— 天气 API 调用
5.2 配置 API Key
copy .env.example .env
# 编辑 .env,填入 DASHSCOPE_API_KEY
.env 示例:
DASHSCOPE_API_KEY=sk-xxxxxxxx
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-plus
API Key 获取:https://dashscope.console.aliyun.com/
5.3 运行
.\run.ps1
# 或
.venv\Scripts\activate
python agent.py
常见报错:直接运行
python agent.py若报No module named 'langchain_openai',说明没激活虚拟环境。务必先activate或使用run.ps1。
六、工具是怎么定义的?
LangChain 用 @tool 装饰器把普通函数变成 LLM 可调用的工具。docstring 非常关键——LLM 靠它判断什么时候该调用。
6.1 天气工具:get_weather
@tool
def get_weather(city: str) -> str:
"""查询指定城市的当前天气。当用户询问某地天气、气温、是否下雨时使用此工具。
Args:
city: 城市名称,如「北京」「上海」「深圳」
"""
resp = requests.get(f"https://wttr.in/{city}", params={"format": "j1"}, ...)
# 解析 JSON,返回「北京:晴,气温 25°C,湿度 60%...」
要点:
- 使用 wttr.in 免费 API,无需额外 Key
- 工具内做异常处理,失败时返回错误信息而非抛异常
- 通过
log_tool_call记录 Trace 日志
6.2 计算器:calculator
@tool
def calculator(expression: str) -> str:
"""计算数学表达式。当用户要求计算、求值、算税费、百分比时使用此工具。"""
tree = ast.parse(expression.strip(), mode="eval")
value = _safe_eval(tree.body) # 基于 AST 的安全计算
return f"{expression} = {value}"
要点:
- 禁止使用
eval(),防止代码注入 - 用
ast模块只允许四则运算和幂运算 - 工业级 Agent 中,工具的安全性是第一优先级
6.3 时间工具:get_current_time(练习成果)
@tool
def get_current_time() -> str:
"""获取当前系统时间。当用户询问现在几点、今天日期、当前时间时使用此工具。"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
注册到 tools/__init__.py:
ALL_TOOLS = [get_weather, calculator, get_current_time]
添加新工具的三步:写函数 → 加 @tool → 注册到 ALL_TOOLS。
七、agent.py 详解:LangGraph 工作流
7.1 状态定义
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
整个 Agent 的状态就是对话历史 messages,包括用户消息、AI 回复、工具调用请求、工具返回结果。
7.2 LLM 绑定工具
def create_llm() -> ChatOpenAI:
return ChatOpenAI(
model=MODEL_NAME,
api_key=DASHSCOPE_API_KEY,
base_url=DASHSCOPE_BASE_URL,
temperature=0,
).bind_tools(ALL_TOOLS)
bind_tools(ALL_TOOLS) 是关键一步:告诉 LLM 它有哪些工具可用,LLM 会在回复中附带结构化的 tool_calls。
7.3 agent 节点:大脑
@trace_node("agent")
def call_agent(state: AgentState) -> AgentState:
llm = create_llm()
messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
response = llm.invoke(messages)
return {"messages": [response]}
System Prompt 约束 LLM 行为:
- 问天气 → 必须用 get_weather,不要编造
- 要计算 → 必须用 calculator,不要心算
- 问时间 → 必须用 get_current_time,不要猜测
- 可以同时使用多个工具
7.4 tools 节点:双手
@trace_node("tools")
def call_tools(state: AgentState) -> AgentState:
return ToolNode(ALL_TOOLS).invoke(state)
ToolNode 是 LangGraph 内置组件,自动根据 agent 的 tool_calls 执行对应函数,并把结果写回 messages。
7.5 构建图:agent ↔ tools 循环
workflow = StateGraph(AgentState)
workflow.add_node("agent", call_agent)
workflow.add_node("tools", call_tools)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", tools_condition) # 关键:条件分支
workflow.add_edge("tools", "agent") # 工具执行完回到 agent
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
tools_condition 是 LangGraph 预置的条件函数:
- LLM 回复包含
tool_calls→ 路由到tools节点 - 否则 → 结束,输出最终回答
这就是 agent ↔ tools 循环 的核心机制。
7.6 完整数据流
┌─────────────────────────────────────────────────────────┐
│ agent.py 工作流 │
│ │
│ 用户输入 → HumanMessage │
│ ↓ │
│ agent: LLM 决策(调工具 or 直接回答) │
│ ↓ │
│ tools_condition 判断 │
│ ├─ 有 tool_calls → tools: 执行工具 → 回到 agent │
│ └─ 无 tool_calls → 输出最终回答 │
└─────────────────────────────────────────────────────────┘
八、Trace 日志:看见 Agent 在想什么
8.1 为什么需要 Trace?
Agent 是个「黑盒」——用户只看到最终回答,看不到中间调了哪些工具、耗时多久。Trace 日志就是监控大屏,记录每一步。
8.2 日志示例
提问:「上海天气如何,再算 5000 的 13% 税费」
[输入] 用户: 上海天气如何,再算 5000 的 13% 税费
>> 进入节点: agent
[决策] Agent 将调用工具: get_weather
<< 完成节点: agent (850 ms)
>> 进入节点: tools
[工具] get_weather({'city': '上海'}) -> 上海:Overcast,气温 18°C...
<< 完成节点: tools (320 ms)
>> 进入节点: agent
[决策] Agent 将调用工具: calculator
<< 完成节点: agent (620 ms)
>> 进入节点: tools
[工具] calculator({'expression': '5000*0.13'}) -> 5000*0.13 = 650
<< 完成节点: tools (5 ms)
>> 进入节点: agent
[决策] Agent 直接回答,无需工具
<< 完成节点: agent (780 ms)
🤖 Agent:上海今天阴天,18°C。5000 的 13% 税费是 650 元。
8.3 日志与节点的对应关系
| 日志 | 对应什么 |
|---|---|
>> 进入节点: agent |
LLM 开始思考 |
[决策] Agent 将调用工具: xxx |
LLM 决定调工具 |
[决策] Agent 直接回答,无需工具 |
LLM 决定不调工具,循环结束 |
>> 进入节点: tools |
开始执行工具 |
[工具] xxx(...) -> 结果 |
工具执行完毕 |
<< 完成节点: xxx (N ms) |
该节点耗时 |
Trace 日志记录工作流,不是工作流本身。没有 Trace,Agent 照样运行;有了 Trace,你才能调试和优化。
九、对话记忆
memory = MemorySaver()
config = {"configurable": {"thread_id": "1"}}
- 多轮对话自动保留上下文(如先问「北京天气」,再问「那上海呢?」)
- 输入
clear会切换thread_id,相当于开新对话 - 输入
exit退出
十、实战测试
10.1 测试用例
| 提问 | 预期行为 | 考察点 |
|---|---|---|
北京天气怎么样 |
调用 get_weather |
单工具 |
算一下 1200 * 0.15 |
调用 calculator |
单工具 |
现在几点了 |
调用 get_current_time |
自定义工具 |
上海天气如何,再算 5000 的 13% 税费 |
依次调用两个工具 | 多轮循环 |
你好 |
直接回答,不调工具 | 条件分支 |
clear |
清空记忆 | 会话管理 |
10.2 理解 agent ↔ tools 循环(学习检验)
用我自己的话总结这个循环:
用户提出问题 → agent(LLM)分析并决定是否调工具 → tools 执行并把结果反馈给 agent → agent 看到结果后继续决策(可能再调工具,也可能直接回答)→ Trace 日志记录全过程。
这个理解是正确的。补充两点:
- 不是每个问题都调工具——「你好」这类问题 agent 会直接回答
- 可以循环多轮——复合问题会 agent → tools → agent → tools → agent → 结束
十一、踩坑记录
11.1 ModuleNotFoundError
ModuleNotFoundError: No module named 'langchain_openai'
原因:没激活虚拟环境。
解决:.venv\Scripts\activate 或 .\run.ps1。
11.2 API Key 未配置
ValueError: 未配置有效的 DASHSCOPE_API_KEY
解决:copy .env.example .env,填入真实 Key。
11.3 看不到 Trace 日志
原因:logging.basicConfig() 被 LangChain 抢先配置,导致日志失效。
解决:项目改用独立的 agent 日志器,每条日志立即 flush,输出到 stdout。
11.4 模型不调用工具,直接编造答案
原因:模型工具调用能力弱,或 System Prompt 约束不够。
解决:
- 换
qwen-plus或qwen-turbo(工具调用能力更强) - 加强 System Prompt:「必须使用工具,不要编造」
11.5 API Key 写死在代码里
最初版本把 Key 写在 agent.py 中,这是安全隐患。
解决:迁移到 .env + python-dotenv,并加入 .gitignore。
十二、学习总结
12.1 Phase 1 通关清单
- 理解 Agent 与普通聊天的区别
- 理解 agent ↔ tools 循环怎么转
- 看懂 Trace 日志在记录什么
- 能独立添加新工具(
get_current_time) - 知道 API Key 为什么要放
.env - 能解释
tools_condition的作用
12.2 关键收获
- Agent = LLM 决策 + 工具执行,不是纯聊天
- @tool + docstring 是工具能被 LLM 正确调用的关键
- agent ↔ tools 循环 可以跑多轮,处理复合任务
- Trace 日志 是调试 Agent 的最佳手段
- 工具安全 很重要(calculator 禁用 eval 就是例子)
- System Prompt 约束 LLM 何时调工具、何时直接回答
12.3 从 Phase 0 到 Phase 1 的进化
test.py(规则 if-else)
→ 纯对话 Agent(只会聊天)
→ 工具 Agent(会调工具)← 本文
→ RAG Agent(会查文档)← Phase 2
→ 合体 Agent(工具 + RAG)← Phase 2.5
十三、下一步:Phase 2 RAG
Phase 1 给 Agent 装上了「手」,但有一类问题工具解决不了:
「我们公司的年假有几天?」「报销流程是什么?」
这些信息在内部文档里,不在 API 里。Phase 2 将学习 RAG(检索增强生成)——给 Agent 装一个「图书馆」。
详见同系列文章:《Phase 2:从零搭建 RAG 文档问答系统》
📎 Phase 2 博文文件:agent-rag/Phase2-RAG学习记录-CSDN博文.md
十四、参考资料
系列文章导航
- Phase 1:工具 Agent(本文)
- Phase 2:RAG 文档问答(LangGraph + Chroma + Embedding)
- Phase 2.5:RAG + 工具 Agent 合体 —— 待发布
- Phase 3+:复杂工作流、多 Agent 协作、生产化部署 —— 规划中
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎在评论区交流。
更多推荐
所有评论(0)