LangChain v1 入门:用 create_agent 跑通第一个智能体(无需 API Key)
大多数入门教程把“跑通”当作终点:贴一段代码,打印一句回答,结束。但真正动手之后,新手遇到的问题往往出现在跑通之后:invoke 返回的到底是什么?模型每一轮收到了哪些消息?为什么换了一种输入写法,程序不报错,问题却没有送到模型?
本页写给刚接触 LangChain v1 的中文开发者:用 create_agent 搭一个会调用工具的智能体,然后把一次调用拆开来看,最后把新手最常踩的坑逐个复现。
为了让每个读者都能原样复现,本页不使用真实模型。 实验里的“模型”是一个脚本化的替身:它按预设顺序回放固定的消息(先决定调用工具,再给出最终回答),同时记录自己每一轮收到了什么。因此不需要 API Key、不联网,每次运行的输出完全一致。代价是:本页不能回答“真实模型会不会选对工具、回答得好不好”,这取决于具体模型,需要你换上真实模型后自己验证(见第五节)。
适用版本与验证信息
| 项目 | 内容 |
|---|---|
| 验证日期 | 2026-10-26 |
| Python | 3.12.12 |
| 包版本 | langchain==1.4.7、langchain-core==1.7.0、langgraph==1.2.17、langchain-openai==1.7.2、openai==3.37.0、pydantic==2.14.1(见下方版本脚本的输出) |
| 模型 | 脚本化替身模型,不联网、不需要 API Key;第四节第 7 项只构造模型对象,不发出任何请求 |
| 官方来源 | Agents (opens in a new tab)、Quickstart (opens in a new tab)、Tools (opens in a new tab),对照官方文档仓库 langchain-ai/docs (opens in a new tab) 提交 b8d4ced(2026-10-16) |
| 实验代码 | 站点仓库 labs/first-agent/,依赖锁定在 labs/requirements/v1.txt |
先确认你的环境和上表一致。下面是本页实验在验证环境里打印的版本,你可以用同样的脚本对照自己的环境:
"""Print the interpreter and package versions the other scripts in this lab were recorded with."""
import importlib.metadata as metadata
import sys
print("python", sys.version.split()[0])
for package in ["langchain", "langchain-core", "langgraph", "langchain-openai", "openai", "pydantic"]:
print(package, metadata.version(package))python 3.12.12
langchain 1.4.7
langchain-core 1.7.0
langgraph 1.2.17
langchain-openai 1.7.2
openai 3.37.0
pydantic 2.14.1先说结论
- 智能体就是“模型在循环里调用工具,直到任务完成”,这是官方文档对 Agent 的定义。
create_agent只需要三样东西:模型、工具列表、系统提示词。 invoke返回的是一个字典,本页的例子里只有一个键messages。最终回答是result["messages"][-1].content,而不是result.content。- 一次“调用工具再回答”的问答,模型被调用了两轮:第 1 轮收到 2 条消息(系统提示词和用户问题),第 2 轮收到 4 条(多了模型自己的工具调用和工具返回值)。每一轮都带着完整历史。
- 模型看不到你的 Python 函数,它只看到一份 JSON 定义:工具名、描述(来自 docstring)、参数名和类型。
- 输入必须写成
{"messages": ...}。 把整个输入写成字符串会报错;而沿用旧版AgentExecutor的{"input": ...},或者传空消息列表,不会报错,但问题根本没有送到模型,模型第 1 轮只收到系统提示词。这是本页最值得记住的一条。 - 有些错误在构造时就暴露,有些要到第一次
invoke才暴露。 模型没有实现bind_tools就属于后者,而且异常没有任何说明文字。
一、最小可运行的智能体
下面的脚本是完整的最小例子:定义一个查天气的工具,创建智能体,问一句“北京天气怎么样?”,再把返回值逐条打印出来。RecordingModel 和 get_weather 定义在同目录的 shared.py(get_weather 用 @tool 声明,是官方快速入门里“天气工具”的中文版本):
"""The smallest useful agent: one tool, one system prompt, one invoke call."""
from langchain.agents import create_agent
from shared import RecordingModel, get_weather, weather_script
model = RecordingModel(script=weather_script()) # stands in for a real chat model
agent = create_agent(model, tools=[get_weather], system_prompt="你是天气助手,用中文简短回答。")
result = agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]})
print("result type:", type(result).__name__)
print("result keys:", list(result))
for message in result["messages"]:
if message.type == "ai" and message.tool_calls:
call = message.tool_calls[0]
print(f"ai: 调用工具 {call['name']}({call['args']})")
else:
print(f"{message.type}: {message.content}")
print("最终回答:", result["messages"][-1].content)result type: dict
result keys: ['messages']
human: 北京天气怎么样?
ai: 调用工具 get_weather({'city': '北京'})
tool: 晴,22°C
ai: 北京现在晴,22°C。
最终回答: 北京现在晴,22°C。逐行看这份输出:
create_agent(model, tools=[...], system_prompt=...):三个参数就是智能体的全部必需配置。官方文档说明,model可以是形如"provider:model"的字符串,也可以是已经构造好的模型对象;本例传的是模型对象,所以不涉及密钥;- 返回值是字典:
result type: dict,result keys: ['messages']。智能体的状态里有一个消息序列,invoke把它整体返回。本例没有配置结构化输出等其他功能,所以只有这一个键; - 消息序列有 4 条:
human(用户问题)、ai(模型决定调用get_weather,参数是{'city': '北京'})、tool(工具的返回值)、ai(模型根据工具结果给出最终回答)。这就是“循环”的一次完整转动:模型 → 工具 → 模型; - 最终回答在最后一条消息里:
result["messages"][-1].content。官方快速入门打印的是.content_blocks,本页只使用.content取文本,没有验证.content_blocks。
二、一次调用里,模型到底收到什么
第一节只看了输出。要真正理解智能体,需要知道模型每一轮收到的输入。下面的脚本让替身模型把自己收到的内容记下来,同时打印工具被转换成什么样的定义:
"""What the model actually receives: the tool definition and the messages of every round."""
import json
from langchain.agents import create_agent
from langchain_core.utils.function_calling import convert_to_openai_tool
from shared import RecordingModel, get_weather, weather_script
print("-- 模型收到的工具定义 --")
print(json.dumps(convert_to_openai_tool(get_weather), ensure_ascii=False, indent=2))
model = RecordingModel(script=weather_script())
agent = create_agent(model, tools=[get_weather], system_prompt="你是天气助手,用中文简短回答。")
agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]})
def show(message):
if message.type == "ai" and message.tool_calls:
return f"ai(tool_calls={[(c['name'], c['args']) for c in message.tool_calls]})"
return f"{message.type}({message.content})"
print("-- 绑定到模型的工具 --")
print(model.bound)
for number, received in enumerate(model.inputs, start=1):
print(f"-- 第 {number} 轮,模型收到 {len(received)} 条消息 --")
for message in received:
print(" ", show(message))-- 模型收到的工具定义 --
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市当前天气。",
"parameters": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
}
}
}
-- 绑定到模型的工具 --
['get_weather']
-- 第 1 轮,模型收到 2 条消息 --
system(你是天气助手,用中文简短回答。)
human(北京天气怎么样?)
-- 第 2 轮,模型收到 4 条消息 --
system(你是天气助手,用中文简短回答。)
human(北京天气怎么样?)
ai(tool_calls=[('get_weather', {'city': '北京'})])
tool(晴,22°C)几个值得注意的点:
- 工具定义只有名称、描述和参数。
description就是get_weather的 docstring查询城市当前天气。;参数city只有类型string,没有任何参数说明。想让模型明白每个参数的含义,要用args_schema或parse_docstring=True,详见工具调用 schema 排障; - 工具被绑定到了模型上:
['get_weather']是create_agent在调用模型前传给bind_tools的工具名; - 第 1 轮 2 条消息,第 2 轮 4 条。 第 2 轮多出来的两条正是第 1 轮的产物:模型的工具调用和工具的返回值。智能体没有“记住”什么,它只是把越来越长的历史整体再发给模型一遍。这也解释了为什么对话越长、工具调用越多,每一轮的输入就越长,通常成本和延迟也随之增加;
- 系统提示词是每一轮的第一条消息。
理解这一点之后,很多现象都有了解释:模型“忘记”了前面的内容,是因为那些消息没有出现在这一轮的输入里;多轮对话要接着聊,需要你让历史保存下来并带上 thread_id,这是下一步要学的内容,见ConversationBufferMemory 迁移到 v1。
三、invoke 的输入:哪些写法能用,哪些会悄悄出错
从旧教程复制代码的新手,最容易在输入的写法上出问题。下面把 7 种常见写法逐个交给同一个智能体,记录三件事:会不会报错;如果不报错,第 1 轮模型收到了哪些消息;用户的问题有没有真的送到模型:
"""Which shapes of invoke() input does create_agent accept, and what happens to the question."""
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from shared import RecordingModel, get_weather, weather_script
QUESTION = "北京天气怎么样?"
CASES = [
("messages 是 dict 列表", {"messages": [{"role": "user", "content": QUESTION}]}),
("messages 是元组列表", {"messages": [("user", QUESTION)]}),
("messages 是 HumanMessage 列表", {"messages": [HumanMessage(QUESTION)]}),
("messages 直接是字符串", {"messages": QUESTION}),
("整个输入是字符串", QUESTION),
('旧写法 {"input": ...}', {"input": QUESTION}),
('空列表 {"messages": []}', {"messages": []}),
]
for label, payload in CASES:
model = RecordingModel(script=weather_script())
agent = create_agent(model, tools=[get_weather], system_prompt="你是天气助手,用中文简短回答。")
try:
agent.invoke(payload)
except Exception as error: # report the exception type and its first line
first_line = (str(error).splitlines() or ["(无说明文字)"])[0]
print(f"{label}: 报错 {type(error).__name__}: {first_line}")
continue
first_round = [f"{m.type}" for m in model.inputs[0]]
sent = QUESTION in "".join(str(m.content) for m in model.inputs[0])
print(f"{label}: 不报错;第 1 轮模型收到 {first_round};问题送达模型: {sent}")messages 是 dict 列表: 不报错;第 1 轮模型收到 ['system', 'human'];问题送达模型: True
messages 是元组列表: 不报错;第 1 轮模型收到 ['system', 'human'];问题送达模型: True
messages 是 HumanMessage 列表: 不报错;第 1 轮模型收到 ['system', 'human'];问题送达模型: True
messages 直接是字符串: 不报错;第 1 轮模型收到 ['system', 'human'];问题送达模型: True
整个输入是字符串: 报错 InvalidUpdateError: Expected dict, got 北京天气怎么样?
旧写法 {"input": ...}: 不报错;第 1 轮模型收到 ['system'];问题送达模型: False
空列表 {"messages": []}: 不报错;第 1 轮模型收到 ['system'];问题送达模型: False把结果整理成一张表:
| 写法 | 结果 | 问题是否送达模型 |
|---|---|---|
{"messages": [{"role": "user", "content": ...}]} | 正常,官方快速入门的写法 | 是 |
{"messages": [("user", ...)]} | 正常 | 是 |
{"messages": [HumanMessage(...)]} | 正常 | 是 |
{"messages": "..."}(直接给字符串) | 正常,被当作一条用户消息 | 是 |
"..."(整个输入是字符串) | 报错 InvalidUpdateError: Expected dict, got ... | 否 |
{"input": "..."}(旧版 AgentExecutor 写法) | 不报错 | 否,模型只收到系统提示词 |
{"messages": []} | 不报错 | 否,模型只收到系统提示词 |
最危险的是最后两行:程序正常运行、正常返回,但模型从头到尾没有看到用户的问题。 本页的替身模型是按脚本回放的,所以仍然“回答”了天气;换成真实模型,它会面对一个只有系统提示词的请求,结果可能是胡乱回答,也可能被模型服务拒绝,具体取决于模型和服务,本页没有测试。
为什么会这样?官方文档说明,调用智能体是向它的状态传入一次更新,所有智能体的状态里都有一个消息序列,实测里携带它的键就是 messages。input 不是这个键,传进去的内容没有成为任何一条消息,框架也没有因此报错。这是实测观察到的行为,官方文档没有专门说明这种情况。
实用建议:
- 不要让业务代码直接拼
invoke的输入。写一个只接受字符串的小函数,统一生成{"messages": [{"role": "user", "content": text}]}; - 从
AgentExecutor迁移过来时,全局搜索"input"键,这类调用不会触发任何异常,只会让回答质量悄悄变差,迁移背景见迁移 Runbook; - 上线后在日志里记录“模型第 1 轮收到的消息里是否有用户消息”,它能直接发现这类问题。
四、新手常见错误对照
下面这组脚本把 7 种新手常见的操作逐个执行,记录报错的类型和第一行文字,以及它是在构造智能体时还是在第一次调用时暴露:
"""Beginner mistakes around create_agent, each reproduced with its exact outcome."""
from langchain.agents import create_agent
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from shared import RecordingModel, get_weather, weather_script
INPUT = {"messages": [{"role": "user", "content": "北京天气怎么样?"}]}
def first_line(error: Exception) -> str:
return (str(error).splitlines() or ["(无说明文字)"])[0]
def report(label: str, action) -> None:
print(label)
try:
outcome = action()
except Exception as error:
print(f" -> {type(error).__name__}: {first_line(error)}")
else:
print(f" -> 成功: {outcome}")
def agent_with(tools):
return create_agent(RecordingModel(script=weather_script()), tools=tools)
class NoToolCallingModel(BaseChatModel):
"""A chat model that never implemented bind_tools()."""
@property
def _llm_type(self) -> str:
return "no-tool-calling"
def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> ChatResult:
return ChatResult(generations=[ChatGeneration(message=AIMessage(content="你好"))])
def plain_function(city: str) -> str:
"""查询城市当前天气。"""
return "晴"
def without_docstring(city: str) -> str:
return "晴"
def read_result_as_message():
result = agent_with([get_weather]).invoke(INPUT)
return result.content
def read_result_correctly():
result = agent_with([get_weather]).invoke(INPUT)
return result["messages"][-1].content
def model_without_tool_calling():
agent = create_agent(NoToolCallingModel(), tools=[get_weather])
print(" (create_agent 返回了,没有报错)")
return agent.invoke(INPUT)["messages"][-1].content
report("1. 把 invoke 的返回值当消息读:result.content", read_result_as_message)
report('2. 正确读法:result["messages"][-1].content', read_result_correctly)
report("3. tools 传了单个工具而不是列表:tools=get_weather", lambda: agent_with(get_weather))
report("4. 不用 @tool,直接传带类型标注和 docstring 的普通函数", lambda: agent_with([plain_function]).invoke(INPUT)["messages"][-1].content)
report("5. 普通函数没有 docstring", lambda: agent_with([without_docstring]))
report("6. 模型没有实现 bind_tools", model_without_tool_calling)
report('7. 用字符串指定模型但没有设置密钥:create_agent("openai:gpt-5.5", ...)', lambda: create_agent("openai:gpt-5.5", tools=[get_weather]))1. 把 invoke 的返回值当消息读:result.content
-> AttributeError: 'dict' object has no attribute 'content'
2. 正确读法:result["messages"][-1].content
-> 成功: 北京现在晴,22°C。
3. tools 传了单个工具而不是列表:tools=get_weather
-> ValueError: The first argument must be a string or a callable with a __name__ for tool decorator. Got <class 'tuple'>
4. 不用 @tool,直接传带类型标注和 docstring 的普通函数
-> 成功: 北京现在晴,22°C。
5. 普通函数没有 docstring
-> ValueError: Function must have a docstring if description not provided.
6. 模型没有实现 bind_tools
(create_agent 返回了,没有报错)
-> NotImplementedError: (无说明文字)
7. 用字符串指定模型但没有设置密钥:create_agent("openai:gpt-5.5", ...)
-> OpenAIError: Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable.对照表:
| 现象 | 原因 | 何时暴露 | 处理 |
|---|---|---|---|
AttributeError: 'dict' object has no attribute 'content' | 把 invoke 的返回值当成消息读 | 读取结果时 | 用 result["messages"][-1].content |
ValueError: The first argument must be a string or a callable ... Got <class 'tuple'> | tools= 传了单个工具而不是列表;报错文字提到 tuple,容易让人摸不着头脑 | 构造时 | 写成 tools=[get_weather] |
ValueError: Function must have a docstring if description not provided. | 工具函数没有 docstring,也没有显式给 description | 构造时 | 补 docstring,或显式传描述;详见工具调用 schema 排障 |
NotImplementedError,没有任何说明文字 | 模型类没有实现 bind_tools,不支持工具调用;create_agent 本身不报错 | 第一次 invoke | 换用支持工具调用的模型;接入前先做验收,见OpenAI 兼容模型接入验收 |
OpenAIError: Missing credentials. ... | 用 "openai:..." 字符串指定模型,但没有设置密钥环境变量 | 构造时 | 在环境变量里设置密钥,不要写进代码;报错文字里列出了可用的环境变量名 |
另外两项没有出错,而是值得记住的好消息:不用 @tool 也行,带类型标注和 docstring 的普通 Python 函数可以直接放进 tools=[...](第 4 项,与官方文档“传入任何可调用对象”的说法一致);而普通函数缺少 docstring 时,和 @tool 一样会在构造时报错(第 5 项)。
这张表里“何时暴露”这一列很重要:构造时暴露的错误最容易处理,一启动就看到;第一次调用才暴露的错误,在测试覆盖不足时会拖到线上。所以哪怕只是入门项目,也建议在启动阶段做一次最小的冒烟调用。
五、换成真实模型,要改哪里
要换成真实模型,只需要改 create_agent 的第一个参数。官方文档给出的写法是 "provider:model" 字符串,例如(这段代码没有在本站运行,因为需要密钥和网络):
from langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-5.5", # 官方文档示例中的模型字符串;可用的模型名以官方文档为准
tools=[get_weather],
system_prompt="你是天气助手,用中文简短回答。",
)密钥通过环境变量提供(第四节第 7 项的报错文字里已经列出变量名),不要写进代码,也不要提交到仓库。如果你使用的是兼容 OpenAI 接口的第三方服务,需要自定义服务地址,接入前的验收清单见OpenAI 兼容模型接入验收。
换模型之后,本页的替身无法替你验证的有三件事,建议按顺序检查:
- 模型真的发出了工具调用吗? 打印
result["messages"],确认有带tool_calls的ai消息,而不是直接回答; - 工具参数合法吗? 参数不合法时,框架会把错误当作
ToolMessage交还给模型,运行不会中断,排查方法见故障诊断中心; - 超时和重试怎么设? 默认配置下的最坏延迟可能出乎意料,见模型调用的延迟预算。
六、本站术语约定
LangChain 的资料中英文夹杂,同一个概念在不同文章里译法不一。下表是本站正文的用法约定,不是官方译法,目的是让你在站内不同页面之间读到一致的词。遇到官方英文资料时,请按左列的英文对照:
| 英文 | 本站用法 | 说明 |
|---|---|---|
| Agent | 智能体;正文常直接写 Agent | 官方定义:模型在循环里调用工具,直到任务完成 |
| tool / tool calling | 工具 / 工具调用 | 工具调用指模型输出“要调用哪个工具、参数是什么”,真正执行工具的是框架 |
| message | 消息 | 分 human(用户)、ai(模型)、tool(工具返回值)、system(系统提示词)等类型 |
| system prompt | 系统提示词 | 每一轮请求的第一条消息 |
| state | 状态 | 智能体的状态里有一个 messages 序列 |
thread / thread_id | 会话 / thread_id | 用同一个 thread_id 才能接着上次的历史,需要配置 checkpointer |
| checkpoint / checkpointer | 检查点 / 检查点存储,正文常保留英文 | 见checkpoint 与故障恢复 |
| middleware | 中间件 | 见Agent 中间件与安全护栏 |
| prompt | 提示词 | |
| trace | 追踪 | 见LangSmith 追踪上线前核对 |
| streaming | 流式输出 | 本页未涉及 |
| structured output | 结构化输出 | 本页未涉及 |
约定的用词会随站点内容增加而补充;某个词在你看来有更好的译法,欢迎通过联系我们提出。
失败与回滚
- 输出和本页不一致:先运行上文的版本脚本,对照版本表。本页的实验锁定了具体版本,其中报错文字(尤其是第四节第 7 项来自
openai包的那一行)会随版本变化; - 回到验证过的版本:
pip install "langchain==1.4.7" "langchain-core==1.7.0" "langgraph==1.2.17",需要同时锁定langchain-openai==1.7.2和openai==3.37.0才能完全复现第四节第 7 项; - 从这里往下走遇到问题:把报错原文拿到故障诊断中心对号入座;
- 局限:本页只验证了框架的行为,没有验证任何真实模型。替身模型不会犯“选错工具”“参数写错”这类错误,真实模型会,所以本页的所有“成功”只代表链路本身是通的。
相关阅读
- 下一步学什么:create_agent 还是 LangGraph?同一任务两种实现的选型决策表;
- 工具定义怎么写:工具调用 schema 排障;
- 想让智能体记住上一轮对话:ConversationBufferMemory 迁移到 v1;
- 上线前的核对:LangChain 生产发布检查表;
- 手里是旧项目:从 v0.x 升级到 v1 的双轨迁移 Runbook。
来源
- Agents (opens in a new tab):Agent 的定义(模型在循环里调用工具)、
model可以是"provider:model"字符串或模型对象、tools接受任何可调用对象、调用智能体是向状态传入更新、状态里有消息序列; - Quickstart (opens in a new tab):
create_agent、天气工具、{"messages": [{"role": "user", ...}]}的调用方式、result["messages"][-1]的读取方式; - Tools (opens in a new tab):工具定义与 docstring 作为描述。
以上来源均于 2026-10-26 核对。本页所有输出来自站点仓库 labs/first-agent/ 在上表版本下的实际运行。