1. 本章目标 #
上一章讲清了 Agent = Model + 驾驭层(Harness):模型负责推理与决策,驾驭层负责「模型 ↔ 工具」循环、提示词,以及后续可叠加的中间件。
概念有了,还缺一次「亲手跑通」——让模型真的去调一个工具,而不是凭训练数据瞎编天气。
本章目标:
跑通一个可调用工具的天气助手,并看懂它的执行轨迹。
学完你应能:
- 用
create_agent创建并调用 Agent - 看懂返回的消息轨迹(为何会调工具、工具结果如何回灌)
- 扩展成多工具助手,理解「选型」靠什么引导
- 用
agent.stream观察节点更新,并可选打开 LangSmith 追踪
参考文档:
跑本章示例前请确认:
- 已安装
langchain、langchain-deepseek、python-dotenv - 项目根目录
.env中有可用的DEEPSEEK_API_KEY - (可选)若要用 LangSmith:配置
LANGSMITH_TRACING与对应 API Key
2. 第一个 Agent:天气助手 #
最小闭环只有三步:
定义工具函数 → create_agent(...) → agent.invoke(...)为什么选「查天气」作第一例?它够简单,又能逼出完整的 Agent 行为:
- 用户问的是事实型问题(今天北京天气),模型不该瞎编
- 必须通过工具拿结果(本章用模拟字符串代替真实 API)
- 模型还要把工具结果组织成自然语言再回复用户
分工记牢:
| 角色 | 做什么 | 不做什么 |
|---|---|---|
| 模型 | 决定是否调工具、填参数、组织最终回答 | 真的去访问天气网站 |
| 工具函数 | 执行真实 / 模拟查询 | 代替模型做决策 |
驾驭层(create_agent) |
调度循环、执行工具、维护消息 | 替你写业务逻辑 |
工具可以是普通 Python 函数。模型能「看懂」它,主要靠两样:类型注解(参数名与类型)和 docstring(工具用途说明)。说明写糊了,模型就容易选错工具或填错参数。
2.1. 1.weather_agent.py #
先整段跑通,再对照 §2.2 看内部循环。
`python agent.py
从 langchain.agents 导入 create_agent,用于创建智能体 #
from langchain.agents import create_agent
从 dotenv 导入 load_dotenv,用于从 .env 加载环境变量 #
from dotenv import load_dotenv
加载 .env 中的密钥等变量;override=True 表示覆盖已存在的同名变量 #
load_dotenv(override=True)
定义获取天气的工具函数:参数 city 为城市名,返回值为字符串 #
def get_weather(city: str) -> str:
# 工具说明(docstring),会作为工具描述提供给模型,帮助它决定何时调用
"""获取指定城市的天气信息。"""
# 返回模拟的天气结果(真实项目里这里会调天气 API)
return f"{city} 今天晴,气温 25°C。"调用 create_agent 创建智能体实例 #
agent = create_agent(
# 指定供应商与模型名:deepseek 的 deepseek-v4-flash
model="deepseek:deepseek-v4-flash",
# 将 get_weather 注册为可调用工具(普通函数即可)
tools=[get_weather],
# 系统提示:设定人设,并要求需要天气时必须调用工具、不要编造
system_prompt="你是一个简洁、可靠的中文助手。需要天气信息时请调用工具,不要编造。",
# 结束 create_agent 调用)
调用智能体处理用户请求,并拿到完整执行结果 #
result = agent.invoke(
# 输入是状态字典;messages 为对话消息列表,角色为 user
{"messages": [{"role": "user", "content": "北京今天天气怎么样?"}]}
# 结束 agent.invoke 调用)
取消息轨迹中的最后一条(通常是助手最终回复),打印其文本内容 #
print(result["messages"][-1].content)
预期类似:
```text
北京今天晴,气温 25°C。读代码时抓住四个点:
load_dotenv:把.env里的密钥注入进程;漏掉这一步,常见报错就是 401。- 工具函数:
city: str+ docstring 决定模型「怎么填参、何时该调」。 create_agent:把模型字符串、工具列表、系统提示绑成一个可invoke的 Agent。- 输入形态:
{"messages": [...]}是状态字典;本章先用 dict 写法,第 4 章再系统讲消息对象。
若模型不调工具、自己编造天气,先检查 system_prompt 是否写明「必须调用工具」,以及工具 docstring 是否清楚。
2.2. 这段代码在做什么 #
一次 invoke 外表像「问一句、答一句」,内部其实可能多步。这正是驾驭层替你省下的手写循环:
用户:「北京今天天气怎么样?」
│
▼
create_agent 循环
│
├─ 模型判断:需要天气 → 调用 get_weather(city="北京")
├─ 工具返回:"北京 今天晴,气温 25°C。"
├─ 模型根据工具结果组织自然语言回答
└─ 结束循环,返回完整 messages对照第 1 章的公式:
- 模型(Model):判断「要不要工具、参数是什么、最终怎么说」
- 驾驭层(Harness):把工具 schema 交给模型、执行
get_weather、把ToolMessage塞回轨迹、决定是否再调模型
要点回顾:
| 参数 / API | 含义 |
|---|---|
model |
"deepseek:deepseek-v4-flash" 或已初始化的模型实例(第 3 章细讲) |
tools |
可调用对象列表;docstring + 类型注解帮助模型选对工具 |
system_prompt |
系统行为设定(人设、何时必须调工具、禁止编造等) |
invoke({...}) |
同步执行一轮(内部可能多步工具调用) |
输入是带 messages 的状态字典;输出也是完整状态。result["messages"][-1].content 只是「最后一句」;真正调试时要看整条轨迹(下一节)。
3. 看懂返回的消息轨迹 #
只打印最后一句,只能确认「好像答对了」,看不出:
- 中间有没有调工具
- 参数对不对(
city是否抽成「上海」而不是整句问句) - 工具结果有没有被模型正确消化
调试 Agent 时,优先看完整 messages。每条消息都是状态的一帧;跑完后这份列表就是把黑盒变白盒的入口。第 4 章会系统讲消息类型,本章先建立阅读习惯。
3.1. inspect_messages.py #
对每条消息关注三件事:
| 看什么 | 为什么重要 |
|---|---|
类型名(HumanMessage / AIMessage / ToolMessage) |
知道这一帧是谁产生的 |
content |
用户原文、工具返回、最终回答多在这里 |
tool_calls(若有) |
模型发起了哪些工具调用、参数是什么 |
# 从 langchain.agents 导入 create_agent,用于创建智能体
from langchain.agents import create_agent
# 从 dotenv 导入 load_dotenv,用于从 .env 加载环境变量
from dotenv import load_dotenv
# 加载 .env 中的密钥等变量;override=True 表示覆盖已存在的同名变量
load_dotenv(override=True)
# 定义获取天气的工具函数:参数 city 为城市名,返回值为字符串
def get_weather(city: str) -> str:
# 工具说明(docstring),供模型理解该工具用途
"""获取指定城市的天气信息。"""
# 返回模拟的天气结果字符串
return f"{city} 今天晴,气温 25°C。"
# 调用 create_agent 创建智能体实例
agent = create_agent(
# 指定使用 DeepSeek 的 deepseek-v4-flash 模型
model="deepseek:deepseek-v4-flash",
# 注册天气工具
tools=[get_weather],
# 系统提示:需要天气信息时请调用工具
system_prompt="你是一个简洁、可靠的中文助手。需要天气信息时请调用工具。",
# 结束 create_agent 调用
)
# 调用智能体,询问上海天气,并拿到完整结果
result = agent.invoke(
# 构造包含用户问题的消息列表
{"messages": [{"role": "user", "content": "上海天气如何?"}]}
# 结束 agent.invoke 调用
)
# 遍历完整消息轨迹,同时拿到索引 i 与消息对象 msg
for i, msg in enumerate(result["messages"]):
# 取出消息类型名,例如 HumanMessage / AIMessage / ToolMessage
msg_type = type(msg).__name__
# 打印当前消息的序号与类型,便于分段阅读轨迹
print(f"\n===== [{i}] {msg_type} =====")
# 安全读取消息的文本内容;没有 content 属性时得到 None
content = getattr(msg, "content", None)
# 若存在文本内容,则打印出来(工具结果、最终回答等多在这里)
if content:
# 打印该消息的文本内容
print("content:", content)
# 安全读取模型发起的工具调用列表;没有则得到 None
tool_calls = getattr(msg, "tool_calls", None)
# 若本条消息包含工具调用请求,则打印出来
if tool_calls:
# 打印 tool_calls 详情(工具名、参数等)
print("tool_calls:", tool_calls)典型轨迹(简化):
[0] HumanMessage → 用户问题
[1] AIMessage → 带 tool_calls:get_weather(city="上海")
[2] ToolMessage → 工具返回字符串
[3] AIMessage → 最终自然语言回答如何读:
- 若缺少带
tool_calls的AIMessage,说明模型没打算调工具(提示词 / docstring 要收紧) - 若有
tool_calls但没有ToolMessage,说明执行或回传环节异常(正常 Agent 路径下少见) - 最终自然语言回答一般在最后一条
AIMessage.content - 若
city抽错(例如抽成整句「上海天气如何」),属于参数抽取问题,可在系统提示里加一句「城市名用短名称」
这就是驾驭层帮你自动完成的「模型 ↔ 工具」循环。第 4 章会系统讲消息类型;第 8 章会深入工具定义与错误处理。
4. 多工具助手 #
真实项目很少只有一个工具。模型要按用户意图在多个工具里选型,甚至一次问题里连调多个。
下面加「天气 + 加法」,观察它如何按需选择(更复杂的多工具协作见第 9 章)。
引导选型靠两样,缺一不可:
- 工具的 名字 + 类型注解 + docstring(模型「看得见」的说明书)
system_prompt里的使用策略(何时调哪个、禁止心算等业务约束)
工具一多,常见失败不是「调不动」,而是「调错」:该用计算器却心算、该查天气却空答。所以多工具阶段要养成:改完提示 / 说明后,用 §3 的轨迹检查实际调用了谁。
4.1. multi_tools_agent.py #
本例用 @tool 显式注册;入门阶段把普通函数直接放进 tools=[...] 往往也行。更关键的是说明写没写清楚。
# 从 langchain.agents 导入 create_agent,用于创建智能体
from langchain.agents import create_agent
# 从 langchain.tools 导入 tool 装饰器,用于把函数注册为工具
from langchain.tools import tool
# 从 dotenv 导入 load_dotenv,用于从 .env 加载环境变量
from dotenv import load_dotenv
# 加载 .env 中的密钥等变量;override=True 表示覆盖已存在的同名变量
load_dotenv(override=True)
# 使用 @tool 装饰器,把下方函数注册为可供 Agent 调用的工具
@tool
# 定义获取天气的工具函数:参数 city 为城市名,返回值为字符串
def get_weather(city: str) -> str:
# 工具说明(docstring),会作为工具描述提供给模型
"""获取指定城市的天气信息。"""
# 返回模拟的天气结果字符串
return f"{city} 今天晴,气温 25°C。"
# 使用 @tool 装饰器,把下方函数注册为可供 Agent 调用的工具
@tool
# 定义整数加法工具:接收两个整数 a、b,返回它们的和
def add(a: int, b: int) -> int:
# 工具说明(docstring),会作为工具描述提供给模型
"""计算两个整数之和。"""
# 返回 a 与 b 的加法结果
return a + b
# 调用 create_agent 创建多工具智能体
agent = create_agent(
# 指定使用 DeepSeek 的 deepseek-v4-flash 模型
model="deepseek:deepseek-v4-flash",
# 同时注册天气工具与加法工具
tools=[get_weather, add],
# 系统提示:说明何时调哪个工具,并禁止心算加法
system_prompt=(
# 设定助手角色为办公助手
"你是办公助手。"
# 查天气时必须调用 get_weather
"查天气时调用 get_weather;"
# 做整数加法时必须调用 add,不要心算
"做整数加法时调用 add,不要心算。"
# 结束 system_prompt 字符串拼接
),
# 结束 create_agent 调用
)
# 一次提问里同时触发两类工具,调用智能体并拿到结果
result = agent.invoke(
# 构造输入状态字典
{
# messages 为对话消息列表
"messages": [
{
# 角色为用户
"role": "user",
# 内容同时包含查天气与做加法两个请求
"content": "北京天气怎么样?另外帮我算 128 + 256。",
# 结束单条用户消息字典
}
# 结束 messages 列表
]
# 结束输入状态字典
}
# 结束 agent.invoke 调用
)
# 打印消息轨迹中最后一条的文本内容(助手最终回复)
print(result["messages"][-1].content)@tool 会把函数名、参数 schema、docstring 整理成模型可见的工具描述。
建议用 §3 的方法打印轨迹:正常情况下你会看到至少两段工具相关步骤(天气一次、加法一次;顺序可能因模型而异)。
观察清单:
- 是否出现
get_weather与add的tool_calls add的参数是否为整数128、256(而不是字符串整句)- 最终回答是否同时覆盖天气与加法结果
5. 流式输出 #
回答较长,或想观察「模型步 / 工具步如何交替」时,用 agent.stream。
注意概念分界(第 3 章还会再对照一次):
| API | 流出来的是什么 | 适合 |
|---|---|---|
model.stream |
模型 token / 文本块 | 打字机式展示纯生成 |
agent.stream |
图节点更新(模型步、工具步等) | 观察 Agent 中间过程 |
第一次看 updates 可能较碎:你会陆续看到模型节点、工具节点的增量字典,而不是一整段干净的最终句子。这是正常现象。
5.1. stream_agent.py #
# 从 langchain.agents 导入 create_agent,用于创建智能体
from langchain.agents import create_agent
# 从 dotenv 导入 load_dotenv,用于从 .env 加载环境变量
from dotenv import load_dotenv
# 加载 .env 中的密钥等变量;override=True 表示覆盖已存在的同名变量
load_dotenv(override=True)
# 定义获取天气的工具函数:参数 city 为城市名,返回值为字符串
def get_weather(city: str) -> str:
# 工具说明(docstring),供模型理解该工具用途
"""获取指定城市的天气信息。"""
# 返回模拟的天气结果字符串
return f"{city} 今天晴,气温 25°C。"
# 调用 create_agent 创建智能体实例
agent = create_agent(
# 指定使用 DeepSeek 的 deepseek-v4-flash 模型
model="deepseek:deepseek-v4-flash",
# 注册天气工具
tools=[get_weather],
# 系统提示:设定为简洁的中文助手
system_prompt="你是一个简洁的中文助手。",
# 结束 create_agent 调用
)
# 以流式方式运行 Agent;stream_mode="updates" 表示按图节点更新推送
for chunk in agent.stream(
# 输入状态字典:用户询问深圳今天天气
{"messages": [{"role": "user", "content": "深圳今天天气?"}]},
# 指定流式模式为 updates;也可用 "values" 查看完整状态快照
stream_mode="updates",
# 结束 agent.stream 调用
):
# 打印每一个流式块(可能包含模型节点或工具节点的增量更新)
print(chunk)stream_mode 常见选择:
"updates":每次推送节点增量,适合观察「刚发生了哪一步」"values":每次推送更完整的状态快照,适合对照整份messages如何增长
实践建议:
- 学习 / 调试中间过程 →
stream+"updates"或"values" - 只要最终文本、脚本批处理 → 继续用
invoke - 产品上要「边出字边显示」、且只要最终答复的文本流 → 第 3 章的
model.stream更合适;Agent 的流式语义更偏「编排过程」
6. 常见问题排查 #
按出现频率排查:
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 模型直接编造天气、不调工具 | 系统提示太弱,或 docstring 不清晰 | 在 system_prompt 明确「必须调用工具」;补全工具说明;用 §3 确认有无 tool_calls |
| 调了工具但城市参数怪异 | 抽取不稳 | 在系统提示约束「城市用短名称」;工具参数名尽量直白(如 city) |
AuthenticationError / 401 |
密钥无效、过期,或未加载 .env |
检查 DEEPSEEK_API_KEY;确认执行了 load_dotenv(override=True) |
create_agent 导入失败 |
LangChain 版本过旧 | pip install -U langchain |
| 找不到 deepseek 集成 | 未装集成包 | pip install -U langchain-deepseek |
| LangSmith 无数据 | 未开启追踪或 key 错误 | 确认 LANGSMITH_TRACING=true 与 API Key |
建议尽早打开 LangSmith Tracing:比起只在终端 print 最后一句,更容易看清每一步工具调用、耗时与失败点。后面 Agent / RAG 变复杂时,这个习惯能少猜很多。
7. 练习 #
每题做完后,尽量用 §3 的方式打印轨迹,而不是只看最终一句。
- 改城市:把用户问题换成「广州和杭州天气分别怎么样?」观察是否多次调用
get_weather(一次一城,或一次请求里带两个调用,取决于模型行为)。 - 加工具:再写一个
get_time(city: str) -> str(可返回固定字符串),让助手能回答「现在几点」;确认轨迹里出现该工具名。 - 收紧人设:把
system_prompt改成「只回答天气;其他问题礼貌拒绝」,测一句无关问题(如「帮我写诗」),看是否拒答且不乱调工具。 - 对照轨迹:用
inspect_messages.py打印完整 messages,标出哪一步是tool_calls,哪一步是ToolMessage。
8. 本章小结 #
- 最小闭环:定义工具 →
create_agent→invoke。 - 工具可以是普通函数;写清 类型注解 + docstring,再用
system_prompt收紧行为。 - 调试看 完整 messages,不要只看最后一句;关注
tool_calls与ToolMessage。 - 多工具时靠「工具说明书 + 系统策略」引导选型,并用轨迹验证。
agent.stream流的是图节点更新;尽早打开 LangSmith,为后面复杂 Agent / RAG 打底。
下一章:Chat Models 聊天模型——统一用 init_chat_model 切换供应商、调参数、做流式输出,并学会把模型实例交给 Agent。