🔌 语言模型抽象
消息是输入,提示是模板,那么"接收消息、吐出回答"的 LLM 本身怎么抽象?本章拆解 BaseLanguageModel 体系,重点掌握 bind_tools(绑工具)和 with_structured_output(结构化输出)——这是 Agent 获得行动力的入口。
本章目标
- 理解
BaseLanguageModel→BaseChatModel/BaseLLM两条继承链 - 掌握
invoke/stream/batch三个核心调用方式 - 会用
bind_tools()把工具绑定到模型(工具调用的标准入口) - 会用
with_structured_output()让模型直接返回结构化数据
两条继承链
LangChain 把所有语言模型抽象在一个根类下,但分两条路线:
Agent 全程都基于 BaseChatModel。它输出 AIMessage(能带 tool_calls),而老的 BaseLLM 只输出字符串。后续所有章节默认指 ChatModel。本章也只讲 ChatModel。
BaseLanguageModel:能力来源
所有模型的根。它最关键的一点是继承自 RunnableSerializable——这意味着模型本身就是个 Runnable(LC5 详解),天生支持 invoke/stream/batch,还能序列化存盘:
class BaseLanguageModel(
RunnableSerializable[LanguageModelInput, LanguageModelOutputVar], ABC
):
"""所有语言模型的抽象基类。"""
cache: BaseCache | bool | None = Field(default=None)
"""是否缓存响应(避免重复调用浪费 token)"""
verbose: bool = Field(default_factory=_get_verbosity)
"""是否打印响应文本(调试用)"""
callbacks: Callbacks = Field(default=None)
"""回调(追踪、流式、可观测)"""
tags: list[str] | None = Field(default=None)
metadata: dict[Any, Any] | None = Field(default=None)
"""给运行追踪打标签 / 元数据"""
custom_get_token_ids: Callable[[str], list[int]] | None = None
"""自定义 tokenizer,用于精确计 token"""
三个核心调用方法
| 方法 | 输入 | 输出 | 源码 | 用途 |
|---|---|---|---|---|
invoke() | 消息/字符串 | AIMessage | chat_models.py:463 | 单次同步调用 |
stream() | 消息/字符串 | Iterator[AIMessageChunk] | chat_models.py:715 | 流式(逐字返回) |
batch() | 输入列表 | 输出列表 | (继承自 Runnable) | 并发处理多个输入 |
invoke/stream/batch 不是 ChatModel 自己写的,而是从 Runnable 基类继承的(LC5 会看到,这是 LCEL 的统一接口)。ChatModel 只需实现底层的 _generate(同步)和 _stream(流式)。这套统一接口让你可以用相同方式调用任何模型。
bind_tools:Agent 行动力的入口 ★
这是本章最重要的方法。没有它,模型就不会主动调工具。它把工具的 JSON Schema 绑定到模型上,返回一个新的 Runnable:
class BaseChatModel(BaseLanguageModel):
def bind_tools(
self,
tools: Sequence[dict | type | Callable | BaseTool],
*,
tool_choice: str | None = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, AIMessage]:
"""把工具绑定到模型。
Args:
tools: 工具序列。可以是 dict(JSON Schema) / type(Pydantic) /
Callable(普通函数) / BaseTool 对象。LC6 会讲怎么定义工具。
tool_choice: 强制选择。
None = 模型自己决定;"any" = 必须调一个工具;
具体工具名 = 强制调那个工具。
Returns:
一个新的 Runnable —— 调用时模型已"知道"这些工具的存在,
输出的 AIMessage 会带 tool_calls。
"""
raise NotImplementedError # 子类(如 ChatOpenAI)具体实现
bind_tools 不修改原模型,而是返回一个新的 Runnable 对象。这是 LangChain 的核心设计哲学——所有配置都通过返回新对象完成(bind/with_config/with_fallbacks),保证对象可安全复用。LC5 LCEL 章会看到这个思想的更多体现。
with_structured_output:结构化输出
当你需要模型返回结构化数据(而不是自由文本),用 with_structured_output。它内部其实就是"绑定一个 schema + 自动解析",但封装得更省心:
class BaseChatModel(BaseLanguageModel):
def with_structured_output(
self,
schema: dict | type,
*,
include_raw: bool = False,
**kwargs: Any,
) -> Runnable[LanguageModelInput, dict | BaseModel]:
"""让模型按给定 schema 输出结构化数据。
schema 可以是: OpenAI 工具 schema / JSON Schema / Pydantic 类。
返回的 Runnable 直接输出 dict 或 Pydantic 对象,省去手动解析。
"""
...
可运行代码
本章代码真正调用模型,需要 OpenAI(或兼容)API Key。pip install langchain-openai,设置 OPENAI_API_KEY 环境变量。
# pip install langchain langchain-openai
# export OPENAI_API_KEY="sk-..."
import os
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel
model = ChatOpenAI(model="gpt-4o-mini")
# ============ ① 三种调用方式 ============
# invoke:单次同步
resp = model.invoke([HumanMessage(content="用一句话介绍Python")])
print("invoke:", resp.content)
# stream:流式(逐字返回 AIMessageChunk)
print("stream: ", end="")
for chunk in model.stream([HumanMessage(content="数到5")]):
print(chunk.content, end="", flush=True)
print()
# batch:并发处理多个输入
results = model.batch(["1+1=?", "2+2=?"])
print("batch:", [r.content for r in results])
# ============ ② bind_tools(工具调用) ============
# 这里用 dict 形式的工具 schema(LC6 会学 @tool 装饰器更优雅)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
model_with_tools = model.bind_tools(tools)
ai_msg = model_with_tools.invoke("北京天气如何?")
print("tool_calls:", ai_msg.tool_calls)
# → [{'name': 'get_weather', 'args': {'city': '北京'}, 'id': 'call_xxx'}]
# 注意 content 可能为空 —— 模型选择调工具而非直接回答
# ============ ③ with_structured_output(结构化输出) ============
class PersonInfo(BaseModel):
name: str
age: int
hobby: str
structured_model = model.with_structured_output(PersonInfo)
person = structured_model.invoke("张三今年25岁,喜欢打篮球。提取信息。")
print("structured:", person) # → PersonInfo(name='张三', age=25, hobby='打篮球')
print("type:", type(person)) # → <class 'PersonInfo'> 直接拿到 Pydantic 对象!
和 F2 章强调的一样:bind_tools 让模型决定调哪个工具,但不会真的执行。模型输出 tool_calls 后,你还需要自己写代码执行工具、把结果用 ToolMessage 喂回去。这个"执行+喂回"的循环,正是 LC8 章 AgentExecutor 自动做的事。
与生产实践对照
| LangChain 概念 | OpenCode 对应 | |
|---|---|---|
| ChatOpenAI / 各厂商 ChatModel | → | provider/provider.ts 的 Provider.Service(用 AI SDK 统一) |
| bind_tools | → | session/tools.ts 把 Tool.Def 包成 AI SDK tool 传给 streamText |
| stream() | → | session/llm.ts 的 LLM.Service.stream() |
| AIMessageChunk 流式 | → | session/llm/ai-sdk.ts 的 text-delta 事件 |
| usage_metadata token 统计 | → | session/session.ts 的 getUsage 成本计算 |
小结
- 两条继承链:
BaseChatModel(输出 AIMessage,主流)和BaseLLM(输出 str,老旧)。Agent 全用 ChatModel。 - 三个核心方法
invoke/stream/batch来自 Runnable,统一了所有模型的调用方式。 bind_tools()把工具绑到模型,返回新 Runnable——Agent 行动力的入口。只绑定不执行。with_structured_output()让模型直接返回结构化数据。
下一章 LC4 · 输出解析器:模型输出的 AIMessage 内容是字符串(或带 tool_calls),怎么变成我们想要的 Python 对象?解析器登场。