🔌 语言模型抽象

消息是输入,提示是模板,那么"接收消息、吐出回答"的 LLM 本身怎么抽象?本章拆解 BaseLanguageModel 体系,重点掌握 bind_tools(绑工具)和 with_structured_output(结构化输出)——这是 Agent 获得行动力的入口。

本章目标

  • 理解 BaseLanguageModelBaseChatModel / BaseLLM 两条继承链
  • 掌握 invoke / stream / batch 三个核心调用方式
  • 会用 bind_tools() 把工具绑定到模型(工具调用的标准入口)
  • 会用 with_structured_output() 让模型直接返回结构化数据

两条继承链

LangChain 把所有语言模型抽象在一个根类下,但分两条路线:

BaseLanguageModel language_models/base.py:165 BaseChatModel chat_models.py:272 · 输出 AIMessage BaseLLM llms.py:296 · 输出 str(传统补全) ChatOpenAI / ChatAnthropic / ...(现代主流) OpenAI / HuggingFace LLM(逐渐少用)
图 LC3.1 · 两条继承链:现代 Agent 几乎全用 BaseChatModel
⚠️ 记住这条主线

Agent 全程都基于 BaseChatModel。它输出 AIMessage(能带 tool_calls),而老的 BaseLLM 只输出字符串。后续所有章节默认指 ChatModel。本章也只讲 ChatModel。

BaseLanguageModel:能力来源

所有模型的根。它最关键的一点是继承自 RunnableSerializable——这意味着模型本身就是个 Runnable(LC5 详解),天生支持 invoke/stream/batch,还能序列化存盘:

📄 langchain_core/language_models/base.py:165 · 模型的根 python
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()消息/字符串AIMessagechat_models.py:463单次同步调用
stream()消息/字符串Iterator[AIMessageChunk]chat_models.py:715流式(逐字返回)
batch()输入列表输出列表(继承自 Runnable)并发处理多个输入
📌 这些方法来自 Runnable

invoke/stream/batch 不是 ChatModel 自己写的,而是从 Runnable 基类继承的(LC5 会看到,这是 LCEL 的统一接口)。ChatModel 只需实现底层的 _generate(同步)和 _stream(流式)。这套统一接口让你可以用相同方式调用任何模型

bind_tools:Agent 行动力的入口 ★

这是本章最重要的方法。没有它,模型就不会主动调工具。它把工具的 JSON Schema 绑定到模型上,返回一个新的 Runnable:

📄 langchain_core/language_models/chat_models.py:2338 · bind_tools python
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)具体实现
model 不会调工具 .bind_tools( [search, calculator] ) model_with_tools(新 Runnable) 调用时输出 AIMessage 带 tool_calls
图 LC3.2 · bind_tools 返回一个"绑定工具后的新模型"
💡 不可变思想

bind_tools 不修改原模型,而是返回一个新的 Runnable 对象。这是 LangChain 的核心设计哲学——所有配置都通过返回新对象完成(bind/with_config/with_fallbacks),保证对象可安全复用。LC5 LCEL 章会看到这个思想的更多体现。

with_structured_output:结构化输出

当你需要模型返回结构化数据(而不是自由文本),用 with_structured_output。它内部其实就是"绑定一个 schema + 自动解析",但封装得更省心:

📄 chat_models.py:2357 · with_structured_output python
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 对象,省去手动解析。
        """
        ...

可运行代码

🚀 需要真实 API Key

本章代码真正调用模型,需要 OpenAI(或兼容)API Key。pip install langchain-openai,设置 OPENAI_API_KEY 环境变量。

📄 lc3_models.py · 模型调用 + 工具绑定 + 结构化输出 python
# 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 对象!
⚠️ 留意:bind_tools 只"绑定",不"执行"

和 F2 章强调的一样:bind_tools 让模型决定调哪个工具,但不会真的执行。模型输出 tool_calls 后,你还需要自己写代码执行工具、把结果用 ToolMessage 喂回去。这个"执行+喂回"的循环,正是 LC8 章 AgentExecutor 自动做的事。

与生产实践对照

LangChain 概念OpenCode 对应
ChatOpenAI / 各厂商 ChatModelprovider/provider.ts 的 Provider.Service(用 AI SDK 统一)
bind_toolssession/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 成本计算

小结

下一章

下一章 LC4 · 输出解析器:模型输出的 AIMessage 内容是字符串(或带 tool_calls),怎么变成我们想要的 Python 对象?解析器登场。