摘要:本文记录 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 SDK
  • python-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 日志记录全过程。

这个理解是正确的。补充两点:

  1. 不是每个问题都调工具——「你好」这类问题 agent 会直接回答
  2. 可以循环多轮——复合问题会 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-plusqwen-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 关键收获

  1. Agent = LLM 决策 + 工具执行,不是纯聊天
  2. @tool + docstring 是工具能被 LLM 正确调用的关键
  3. agent ↔ tools 循环 可以跑多轮,处理复合任务
  4. Trace 日志 是调试 Agent 的最佳手段
  5. 工具安全 很重要(calculator 禁用 eval 就是例子)
  6. 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 协作、生产化部署 —— 规划中

如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎在评论区交流。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐