上一篇我们把第一个 Agent 跑了起来:一个模型、一个工具、一个循环。跑通之后再回头看,你会发现 Agent 的每一种高级能力——规划、反思、调用工具——最终都落在同一个原子操作上:把一组消息发给大语言模型(LLM),拿回一条回复。这一篇我们下沉一层,把这个原子操作拆开讲透:模型怎么初始化、消息长什么样、参数怎么调、怎么流式输出、怎么算钱、怎么换供应商。
理解了这一层,后面 LangGraph 的图、MCP 的工具、多 Agent 的编排,本质上都只是往这个原子操作上叠流程。本文按"统一入口、消息类型、参数与容错、流式输出、多模态、计费与选型、多供应商切换"的顺序展开,代码基于 langchain 1.x(langchain-core 1.6),全部可运行。
1、统一模型调用入口
先看不用框架的处境。OpenAI、Anthropic、Google 三家 SDK 的认证方式、客户端类、消息格式、流式事件全都不一样,DeepSeek 和 Ollama 又各自一套。每接一家供应商就要学一遍新 API,换模型约等于重写调用层。
LangChain 的解法是 chat model 抽象:所有模型对外暴露同一套接口——invoke 发消息、stream 流式、bind_tools 绑工具,实例创建统一走 init_chat_model 入口。依赖安装如下:
# 系列项目用 uv 管理依赖,pip 等价
uv add langchain langchain-openai
# 其他供应商按需安装
uv add langchain-anthropic langchain-deepseek langchain-ollama
第一行装的是 init_chat_model 本体和 OpenAI 集成,其余按用到的供应商安装,不必把全世界的包都拖进项目。最基础的调用如下:
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-5")
resp = model.invoke("用一句话解释什么是 Agent")
print(resp.content)
"openai:gpt-5" 是 provider:model 格式的模型串;只写裸模型名时 LangChain 会按前缀推断供应商(gpt- 推 openai、claude- 推 anthropic),官方建议显式写全,行为更可控。这段代码没有出现任何 API Key,因为集成包默认从 OPENAI_API_KEY 等环境变量读取。
这套抽象真正值钱的地方是运行时切换:
model = init_chat_model(
"openai:gpt-5",
configurable_fields=("model", "model_provider"),
)
q = "用一句话解释什么是 token"
print(model.invoke(q).content) # 默认走 GPT-5
# 运行时切到 Claude,业务代码一个字不改
print(model.invoke(q, config={"configurable": {
"model": "anthropic:claude-sonnet-5-5",
}}).content)
换模型只改一个字符串。同一个对象、同一份调用代码,配置不同就走不同供应商,返回值都是统一的 AIMessage。做 A/B 对比、按任务路由模型、线上降级切换,都建立在这个能力上。完整参数见官方参考文档。
还有一层价值容易被忽略:chat model 实现了 Runnable 协议,invoke、stream、batch 天然可组合,后面篇章里的链、图、事件流,都是把模型当标准积木拼出来的。上一篇里 LangSmith 能画出完整的调用链,靠的也是这套协议——四家供应商的请求在追踪里长一个样,排障成本被大幅摊平。
2、四种消息类型
模型 API 是无状态的:每次调用都要把完整对话历史发过去,所谓历史就是一张消息列表。langchain-core 用四个类覆盖一张对话里的全部角色,这是所有后续内容的地基。
| 消息类型 | 对应角色 | 用途 |
|---|---|---|
SystemMessage |
system | 全局人设与规则,只出现一次且在最前 |
HumanMessage |
user | 用户输入与外部指令 |
AIMessage |
assistant | 模型回复,可携带 tool_calls |
ToolMessage |
tool | 工具执行结果,回传给模型 |
四种消息的构造示例如下,其中 AIMessage 和 ToolMessage 平时由框架在工具循环里自动产生,这里手工写出来便于看清结构:
from langchain_core.messages import (
SystemMessage, HumanMessage, AIMessage, ToolMessage,
)
msgs = [
SystemMessage(content="你是天气助手,回答控制在 50 字以内"),
HumanMessage(content="北京今天天气怎么样?"),
# 模型决定调工具:tool_calls 记录函数名与参数
AIMessage(content="", tool_calls=[
{"name": "get_weather", "args": {"city": "北京"}, "id": "call_001"},
]),
# 你的代码执行工具后,用相同 id 把结果回传
ToolMessage(content="晴,西北风 3 级,12℃", tool_call_id="call_001"),
HumanMessage(content="那适合跑步吗?"),
]
print(model.invoke(msgs).content)
三点值得注意。第一,多轮对话的本质就是不断往列表尾部追加消息,所谓"记忆"就是这张列表本身。第二,ToolMessage 的 tool_call_id 必须与它响应的那条 tool_calls 里的 id 配对,模型靠它知道哪次调用对应哪份结果。第三,SystemMessage 是塑造 Agent 行为最便宜的杠杆,放在列表最前面,每次请求都会重发。工具调用的完整闭环是系列第 5 篇的主题,这里先记住消息的形状;调试时对每条消息调 pretty_print(),角色前缀一目了然,比裸打印清爽得多。
3、调用参数与容错
先说一个正在发生的变化:temperature 正在退出历史舞台。GPT-5 系列把 temperature 固定为 1,传其他值直接报错;Claude API 自 2026 年 10 月起对所有模型拒绝 temperature、top_p、top_k 参数。新一代推理模型靠思考深度控制输出风格,而不是采样随机性,写新代码别再把 temperature 当核心调参手段。真正需要记住的是这几个:
model = init_chat_model(
"anthropic:claude-sonnet-5-5",
max_tokens=1024, # 输出上限,兼作单次成本上限
timeout=30, # 单次请求超时,单位秒
max_retries=2, # SDK 层对 429/5xx 自动重试
)
# Runnable 层重试:指数退避,最多 3 次
robust = model.with_retry(
stop_after_attempt=3, wait_exponential_jitter=True,
)
print(robust.invoke("用三句话介绍 HTTP 重试的常见策略").content)
max_tokens 仍是各供应商通用的参数,生产上建议按任务类型给硬上限:分类抽取给 256,写代码给 8192,防止异常回复烧钱。timeout 和 max_retries 会透传给底层 SDK;with_retry 则是 LangChain Runnable 层的通用能力,可叠加指数退避。两层重试别同时拉满,总等待时间会变成超时乘重试次数的乘积,正常配置选一层做主力即可。跨模型兜底用 with_fallbacks([备用模型]),主模型报错时自动切换,容错一节到生产化篇再展开。
4、流式输出
非流式调用要等全部 token 生成完才返回,长回复动辄几十秒白屏。流式把生成过程切成小块,首 token 一到就上屏,打字机效果就是这么来的:
model = init_chat_model("anthropic:claude-sonnet-5-5", max_tokens=512)
for chunk in model.stream([HumanMessage(content="写一段 100 字的产品发布文案")]):
print(chunk.content, end="", flush=True) # 逐 token 上屏
print() # 收尾换行
每个 chunk 是 AIMessageChunk,把所有块的内容拼接起来与非流式的 invoke 结果等价,只是拿到的时间粒度不同。两个实践要点:一是流式下 usage_metadata 挂在最后一个 chunk 上,但 OpenAI 需要显式传 stream_usage=True 才返回;二是生产服务建议默认走流式,长输出既改善体验,又天然规避了 HTTP 超时——异步接口是 astream,配合 FastAPI 的 StreamingResponse 即可。Agent 场景下,流式还是"过程可见性"的基础,后面讲 LangGraph 事件流时会反复用到。最后提醒一点:chunk 不等于 token,供应商通常按批次下发,"逐 token"只是体感描述,更细的控制粒度要到第 13 篇的事件流才有。
5、多模态图片输入
让模型"看图"不需要换接口,只是把 HumanMessage 的 content 从字符串换成内容块列表。URL 和 base64 两种传法如下:
import base64
from langchain_core.messages import HumanMessage
# 方式一:图片 URL
url_msg = HumanMessage(content=[
{"type": "text", "text": "这张截图里有什么问题?"},
{"type": "image_url", "image_url": {"url": "https://example.com/ui.png"}},
])
# 方式二:本地文件转 base64 内嵌
with open("screenshot.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
b64_msg = HumanMessage(content=[
{"type": "text", "text": "这个页面上有几处按钮?"},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
])
model = init_chat_model("google_genai:gemini-3.1-pro-preview")
print(model.invoke([b64_msg]).content)
这个块列表格式是 LangChain 的统一多模态格式,各家集成包会自动转换成自家协议,代码因此保持供应商无关。计费上图片按分辨率折算成 token,一张截图通常几百到一两千 token,直接计入输入成本。Agent 里读 UI 截图、看报错图片、分析图表都是常见用法;内网图片走 base64,公网可访问的走 URL。注意各家模型的多模态支持范围不同,有的能看图不支持音频,有的对图片尺寸和数量有上限,切换供应商前先查一遍支持矩阵,别等运行时才收到 400。
6、token 计费与选型
每次调用的用量藏在返回消息的 usage_metadata 里,langchain-core 把它标准化成统一字段,四家供应商同一个口径:
PRICE = {"input": 2.0, "output": 10.0} # Sonnet 5.5,美元/百万 token
resp = model.invoke([HumanMessage(content="解释一下什么是上下文窗口")])
usage = resp.usage_metadata
# {'input_tokens': 15, 'output_tokens': 102, 'total_tokens': 117, ...}
cost = (usage["input_tokens"] / 1e6 * PRICE["input"]
+ usage["output_tokens"] / 1e6 * PRICE["output"])
print(f"本次调用成本约 ${cost:.6f}")
两个数字要刻进脑子里。第一,输出单价通常是输入的 5 倍,压缩输出长度比压缩输入更省钱;第二,缓存命中价更低,GPT-5 缓存输入只要 $0.125/百万 token,重复前缀(系统提示、工具定义)值得专门做缓存设计。
再拿 Agent 的成本结构算笔账:一个 10 轮的工具循环,固定前缀(系统提示加工具定义)2000 token、每轮新增 500 token,末轮单次输入就有 6500 token 上下,全程累计输入超过 4 万——这还只是一次任务。每轮工具往返都重发全部历史,成本随轮数线性上涨,所以长任务的省钱手段是缓存前缀、压缩历史,而不是简单换个便宜模型了事。
模型选型速查如下(价格为 2026 年 10 月官方定价快照,单位为美元/百万 token):
| 模型 | 输入 | 输出 | 定位 |
|---|---|---|---|
| GPT-5 nano | $0.05 | $0.40 | 高频杂活:分类、抽取、路由 |
| Claude Haiku 5.5 | $0.10 | $0.50 | 杂活,质量更稳 |
| GPT-5 mini | $0.25 | $2.00 | 轻量任务 |
| Gemini 3 Flash | $0.50 | $3.00 | 轻量任务,多模态强 |
| DeepSeek Flash | $0.15 起 | $0.60 起 | 超低价,1M 上下文 |
| GPT-5 | $1.25 | $10.00 | 主力 |
| Claude Sonnet 5.5 | $2.00 | $10.00 | 主力,Agent 首选 |
| Gemini 3.1 Pro | $2.00 | $12.00 | 主力,多模态 |
| DeepSeek V4 Pro | $0.66 起 | $1.98 起 | 主力,性价比突出 |
| Claude Opus 5.5 | $4.00 | $20.00 | 复杂决策 |
| GPT-5.5 | $5.00 起 | $30.00 起 | 复杂决策 |
标"起"的条目存在分级或分时计价:DeepSeek 分峰谷时段,Gemini Pro 按上下文长度分档。各家调价频繁,落地前以 OpenAI、DeepSeek、Gemini 的定价页和 Claude 官方文档为准。
选型原则一句话:便宜模型干杂活,强模型做决策。Agent 的调用量里九成是简单步骤——路由、抽取、格式化、判断是否继续——一成的关键节点才需要强模型的推理能力。文档批量抽取用 Haiku,任务规划与代码生成用 Sonnet,两者单价差 20 倍,混合调用的总成本能比全用主力模型低一个量级。
7、多供应商切换
把前面的能力合起来:同一个函数,四类供应商的对照代码。差异全部收敛在模型串和环境变量里:
from langchain.chat_models import init_chat_model
# OpenAI:读环境变量 OPENAI_API_KEY
gpt = init_chat_model("openai:gpt-5")
# Anthropic:读环境变量 ANTHROPIC_API_KEY
claude = init_chat_model("anthropic:claude-sonnet-5-5")
# DeepSeek:读环境变量 DEEPSEEK_API_KEY
ds = init_chat_model("deepseek:deepseek-v4-pro")
# 本地 Ollama:不需要 Key,默认 http://localhost:11434
local = init_chat_model("ollama:qwen3:8b")
# 四个模型的调用方式完全一致
for m in (gpt, claude, ds, local):
print(m.invoke("一句话自我介绍").content)
四段初始化之外没有一行分支代码,这正是第 1 节抽象的价值。三点补充:
- Key 一律走环境变量,绝不写进代码和仓库
- DeepSeek V4 Pro 输出价约为 GPT-5 的四分之一,高频任务迁移立省成本
ollama pull qwen3:8b拉下模型即可离线推理,数据不出本机
企业自建的 vLLM 集群暴露 OpenAI 兼容接口,接入方式是 init_chat_model("openai:模型名", base_url="http://localhost:8000/v1", api_key="EMPTY")。本地模型的代价是效果上限和并发吞吐,拿它做开发调试、把强模型留给关键路径,是常见的组合。
8、总结
这一篇把 LLM 调用拆到了原子层面:init_chat_model 用一个字符串屏蔽供应商差异;四种消息类型表达完整对话;max_tokens 管成本、with_retry 管容错;stream 出打字机、usage_metadata 出账单;图片走内容块列表;选型上便宜模型干杂活、强模型做决策。这些是后面所有篇章的地基。
三个练习巩固一下:
- 把上一篇 Agent 的模型换成
deepseek:deepseek-v4-pro,验证只改一个字符串还能跑 - 给流式打字机加统计:结束后打印本次
usage_metadata和估算成本 - 用 base64 传一张 UI 截图,让模型列出可访问性问题
对于刚开始写 Agent 的读者,建议先把 invoke、stream、usage_metadata 三件事用同一家供应商跑熟,再横向切两家对比效果与成本,形成手感。下一篇我们讲 Prompt 模板与结构化输出——让模型的输入可复用、输出可解析,Agent 的"血管"就从这里开始生长。


