跳至内容

LangChain v1 入门:用 create_agent 跑通第一个 Agent(无需 API Key)

大多数入门教程把“跑通”当作终点:贴一段代码,打印一句回答,结束。但真正动手之后,新手遇到的问题往往出现在跑通之后:invoke 返回的到底是什么?模型每一轮收到了哪些消息?为什么换了一种输入写法,程序不报错,问题却没有送到模型?

刚接触 LangChain v1 时,可以先用 create_agent 搭一个会调用工具的 Agent(智能体),再把一次调用拆开来看,最后逐个复现新手最常踩的坑。

示例不使用真实模型。 “模型”是一个按预设顺序回放消息的脚本化替身(先决定调用工具,再给出最终回答),并记录自己每一轮收到了什么:不需要 API Key、不联网,每次输出一致。它说明不了真实模型会不会选对工具,这一点需要你换上真实模型后自己验证,见第五节。

适用范围

项目内容
Python3.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、Quickstart、Tools,对照官方文档仓库 langchain-ai/docs 提交 b8d4ced(2026-10-16)
实验代码站点仓库 labs/first-agent/,依赖锁定在 labs/requirements/v1.txt
  • 覆盖:create_agent 的最小用法、invoke 的输入与返回结构、一次“调用工具再回答”里模型每一轮收到的消息,以及新手常见错误的实际报错。
  • 未覆盖:真实模型(替身模型不会犯“选错工具”“参数写错”这类错误,文中的“成功”只代表链路本身是通的);.content_blocks 的读取方式。

先确认你的环境和上表一致。下面是文中实验打印的版本,你可以用同样的脚本对照自己的环境:

"""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 前要知道的六件事

  1. Agent 就是“模型在循环里调用工具,直到任务完成”,这是官方文档对 Agent 的定义。create_agent 只需要三样东西:模型、工具列表、系统提示词。
  2. invoke 返回的是一个字典,文中的例子里只有一个键 messages。最终回答是 result["messages"][-1].content,而不是 result.content。
  3. 一次“调用工具再回答”的问答,模型被调用了两轮:第 1 轮收到 2 条消息(系统提示词和用户问题),第 2 轮收到 4 条(多了模型自己的工具调用和工具返回值)。每一轮都带着完整历史。
  4. 模型看不到你的 Python 函数,它只看到一份 JSON 定义:工具名、描述(来自 docstring)、参数名和类型。
  5. 输入必须写成 {"messages": ...}。 把整个输入写成字符串会报错;而沿用旧版 AgentExecutor 的 {"input": ...},或者传空消息列表,不会报错,但问题根本没有送到模型,模型第 1 轮只收到系统提示词。这是最值得记住的一条。
  6. 有些错误在构造时就暴露,有些要到第一次 invoke 才暴露。 模型没有实现 bind_tools 就属于后者,而且异常没有任何说明文字。

一、最小可运行的 Agent

下面的脚本是完整的最小例子:定义一个查天气的工具,创建 Agent,问一句“北京天气怎么样?”,再把返回值逐条打印出来。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=...):三个参数就是 Agent 的全部必需配置。官方文档说明,model 可以是形如 "provider:model" 的字符串,也可以是已经构造好的模型对象;本例传的是模型对象,所以不涉及密钥;
  • 返回值是字典:result type: dict,result keys: ['messages']。Agent 的状态里有一个消息序列,invoke 把它整体返回。本例没有配置结构化输出等其他功能,所以只有这一个键;
  • 消息序列有 4 条:human(用户问题)、ai(模型决定调用 get_weather,参数是 {'city': '北京'})、tool(工具的返回值)、ai(模型根据工具结果给出最终回答)。这就是“循环”的一次完整转动:模型 → 工具 → 模型;
  • 最终回答在最后一条消息里:result["messages"][-1].content。官方快速入门打印的是 .content_blocks,这里只使用 .content 取文本,.content_blocks 不在本次验证范围。

二、一次调用里,模型到底收到什么

第一节只看了输出。要真正理解 Agent,需要知道模型每一轮收到的输入。下面的脚本让替身模型把自己收到的内容记下来,同时打印工具被转换成什么样的定义:

"""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 轮的产物:模型的工具调用和工具的返回值。Agent 没有“记住”什么,它只是把越来越长的历史整体再发给模型一遍。这也解释了为什么对话越长、工具调用越多,每一轮的输入就越长,通常成本和延迟也随之增加;
  • 系统提示词是每一轮的第一条消息。

理解这一点之后,很多现象都有了解释:模型“忘记”了前面的内容,是因为那些消息没有出现在这一轮的输入里;多轮对话要接着聊,需要你让历史保存下来并带上 thread_id,这是下一步要学的内容,见ConversationBufferMemory 迁移到 v1。

三、invoke 的输入:哪些写法能用,哪些会悄悄出错

从旧教程复制代码的新手,最容易在输入的写法上出问题。下面把 7 种常见写法逐个交给同一个 Agent,记录三件事:会不会报错;如果不报错,第 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": []}不报错否,模型只收到系统提示词

最危险的是最后两行:程序正常运行、正常返回,但模型从头到尾没有看到用户的问题。 替身模型按脚本回放,所以仍然“回答”了天气;换成真实模型,它会面对一个只有系统提示词的请求,结果可能是胡乱回答,也可能被模型服务拒绝,具体取决于模型和服务,这一点没有测试。

为什么会这样?官方文档说明,调用 Agent 是向它的状态传入一次更新,所有 Agent 的状态里都有一个消息序列,实测里携带它的键就是 messages。input 不是这个键,传进去的内容没有成为任何一条消息,框架也没有因此报错。这是实测观察到的行为,官方文档没有专门说明这种情况。

实用建议:

  • 不要让业务代码直接拼 invoke 的输入。写一个只接受字符串的小函数,统一生成 {"messages": [{"role": "user", "content": text}]};
  • 从 AgentExecutor 迁移过来时,全局搜索 "input" 键,这类调用不会触发任何异常,只会让回答质量悄悄变差,迁移背景见迁移 Runbook;
  • 上线后在日志里记录“模型第 1 轮收到的消息里是否有用户消息”,它能直接发现这类问题。

四、新手常见错误对照

下面这组脚本把 7 种新手常见的操作逐个执行,记录报错的类型和第一行文字,以及它是在构造 Agent 时还是在第一次调用时暴露:

"""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 兼容模型接入检查。

换成真实模型后,脚本化模型验证不了的有三件事,建议按顺序检查:

  1. 模型真的发出了工具调用吗? 打印 result["messages"],确认有带 tool_calls 的 ai 消息,而不是直接回答;
  2. 工具参数合法吗? 参数不合法时,框架会把错误当作 ToolMessage 交还给模型,运行不会中断,排查方法见故障速查;
  3. 超时和重试怎么设? 默认配置下的最坏延迟可能出乎意料,见模型调用的延迟预算。

六、你会看到的几个名字

LangChain 的资料中英文夹杂,同一个概念在不同文章里译法不一。下表是文中的用词约定,不是官方译法,用来保证各篇读到一致的词。

展开术语对照表

遇到官方英文资料时,请按左列的英文对照:

英文文中用法说明
Agent保留英文,不译官方定义:模型在循环里调用工具,直到任务完成
tool / tool calling工具 / 工具调用工具调用指模型输出“要调用哪个工具、参数是什么”,真正执行工具的是框架
message消息分 human(用户)、ai(模型)、tool(工具返回值)、system(系统提示词)等类型
system prompt系统提示词每一轮请求的第一条消息
state状态Agent 的状态里有一个 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 项;
  • 从这里往下走遇到问题:把报错原文拿到故障速查对号入座;

依据与复现

  • Agents:Agent 的定义(模型在循环里调用工具)、model 可以是 "provider:model" 字符串或模型对象、tools 接受任何可调用对象、调用 Agent 是向状态传入更新、状态里有消息序列;
  • Quickstart:create_agent、天气工具、{"messages": [{"role": "user", ...}]} 的调用方式、result["messages"][-1] 的读取方式;
  • Tools:工具定义与 docstring 作为描述;
  • 复现:先执行 labs/setup-envs.sh v1 重建环境,再执行 python3 labs/run.py --lab first-agent;
  • 输出范围:全部输出来自站点仓库 labs/first-agent/ 在“适用范围”所列版本下的实际运行;
  • 触发更新:langchain 主版本变化、create_agent 的输入或工具参数行为变化、官方快速入门的写法变化;
  • 官方来源核对于 2026-10-26。

继续阅读

最后更新于