> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

# 01 · 第一次对话：为什么所有大模型都长一个样

> 为什么所有大模型都长一个样：LangChain 的统一调用接口 invoke()，换模型只改三行。

# 01 · 第一次对话：为什么所有大模型都长一个样

## 开头的现象

小林第一次跑通大模型对话，是在一个周日下午。他按照 DeepSeek 的文档，用 Python 的 requests 库手写了三十多行代码：构造 headers、拼 JSON、发 POST 请求、解析返回……跑通的那一刻，控制台打出了"你好，我是 DeepSeek！"——他高兴了不到三秒钟，老板的微信就来了："小林，咱们准备换用智谱的模型，你评估一下。"

小林盯着屏幕上那三十行代码，心里咯噔一下：换模型？那这三十行是不是全得重写？

## 第一幕：小林的手写 API 调用，和他的"换模型恐惧症"

小林的第一个版本长这样——他当时觉得写得很漂亮：

```python theme={null}
# 小林手写的 DeepSeek 调用（第 1 版，自我感觉良好）
import requests

resp = requests.post(
    "https://api.deepseek.com/chat/completions",   # DeepSeek 的地址
    headers={"Authorization": "Bearer sk-xxx", "Content-Type": "application/json"},
    json={
        "model": "deepseek-chat",
        "messages": [{"role": "user", "content": "你好"}],
        "temperature": 0.7,
        "max_tokens": 1024,
    },
)
data = resp.json()
print(data["choices"][0]["message"]["content"])   # 取回复：层层剥开 choices → message → content
```

他换到智谱，去查智谱的文档。结果发现：

* 地址变成了 `https://open.bigmodel.cn/api/anthropic/v1/messages`（连协议都换了）
* 请求头从 `Authorization` 变成了 `x-api-key`
* `messages` 里的写法变了，`max_tokens` 居然变成**必填**的
* 取回复的方式变成了 `data["content"][0]["text"]`——不是 `choices[0].message.content` 了！

小林把脸埋进手里，心里默念：**"我只是想换个供应商，怎么像是换了一门语言？"**

他当时的默认假设是：**"API 就是长这样的，模型供应商不同，我就得学不同的 API。"** 这个假设让他想到一个恐怖的前景——公司每换一次模型，他就要重写一遍调用代码。他在脑海里已经看到了自己未来十年在 Ctrl+C / Ctrl+V 里度过的样子。

> 想亲眼看这个"换供应商 = 换 API"的恐怖吗？跑一遍上面那三十行 DeepSeek 代码，然后把 base\_url 换成智谱的——你会收到一个四百多行的报错，小林当时盯着它看了十分钟。

## 第二幕：小林发现了一个"统一插口"

小林在翻智谱文档的时候，看到了一行字：**"我们提供 Anthropic 兼容接口"**。他一开始没当回事，直到他注意到——智谱、Claude 官方、还有一堆别的平台，居然都认同一个接口协议。他脑子里闪过一个念头：那有没有可能，大家其实在说同一门语言，只是我没找到那个"翻译官"？

他在网上搜"LangChain"，看到它的自我介绍只有一句话：

> **LangChain 为所有大模型提供统一的调用接口——你写一次代码，换模型只改一个参数。**

小林的耳朵竖起来了。他装了 `langchain` 和 `langchain-anthropic` 两个包，然后写了他人生中第一段 LangChain 代码——只有几行：

```python theme={null}
# 小林的第一段 LangChain 代码（换供应商只改 3 行）
from dotenv import load_dotenv
load_dotenv()                                  # 从 .env 读密钥
from langchain_anthropic import ChatAnthropic  # 智谱/Claude 走这个包

model = ChatAnthropic(
    model="glm-4.7",                           # ① 模型名
    base_url="https://open.bigmodel.cn/api/anthropic",   # ② 平台地址
    api_key=os.environ["ZHIPU_API_KEY"],       # ③ 密钥（从环境变量读，不写进代码）
    max_tokens=1024,
)

resp = model.invoke("你好")                     # 就一个方法：invoke
print(resp.content)                             # 就一个字段：content
```

他盯着这段代码看了半天，心里涌起一股不真实感：**三十行手写代码，变成六行了？** 他决定验证一件事——把 `ChatAnthropic` 换成 `ChatDeepSeek`，把 `glm-4.7` 换成 `deepseek-v4-flash`，把 base\_url 换成 DeepSeek 的。他战战兢兢地按了运行，结果……跑通了。一模一样地跑通了。

**他这才明白：LangChain 干的事，是把"每个供应商的方言"翻译成"统一的普通话"。** 他之前以为 API 就该各长各样，真相是——**那些供应商早就用着同一套接口协议（OpenAI 兼容 / Anthropic 兼容），LangChain 只是把这套协议包装成了一个统一的 `invoke()`。**

> 想验证"换模型只改三行"吗？在 learn-langchain 环境跑 `01_first_chat.py`，然后把 `ChatAnthropic` 换成 `ChatDeepSeek`、把 `glm-4.7` 换成 `deepseek-v4-flash`——同样的 `invoke()`，同样的 `.content`。

## 第三幕：这个"统一"是怎么实现的——三层结构

小林没有满足于"能用"。他拆开了 LangChain 的包装，发现里面是三层：

```text theme={null}
你的代码
   │  只认 invoke() / .content
   ▼
LangChain 统一接口（langchain_core 里的 BaseChatModel）
   │  定义"长什么样"（接口）
   ▼
协议适配层（langchain_anthropic / langchain_openai）
   │  翻译"怎么连"（把 invoke() 翻译成该平台的 HTTP 请求）
   ▼
真实 API（智谱 / DeepSeek / OpenAI / Claude）
```

* **顶层（你的代码）**：只写 `invoke("你好")`，不关心底层是谁。
* **中间层（协议适配）**：`ChatAnthropic` 内部知道怎么把 `invoke` 翻译成智谱要的 HTTP 请求，`ChatDeepSeek` 知道怎么翻译成 DeepSeek 要的。
* **底层（真实 API）**：各平台原样收请求、原样返回。

小林把这比作 USB 插口："你买任何鼠标、键盘、U 盘，插上去都能用——因为 USB 是统一标准。LangChain 就是 AI 界的 USB 标准，`invoke()` 就是那个插口。"

他还注意到一个细节：为什么 `base_url` 一个要带 `/v1` 一个不要带？因为 `ChatAnthropic` 会自动拼 `/v1/messages`，所以 base\_url 给了它就不要带 `/v1`；而 `ChatOpenAI` 不会自动拼，所以你得自己带 `/v1`。**同一个插口，两个牌子的充电器接法还不一样**——这是小林踩的第一个坑，后面还有一堆。

### 🔧 技术细节：这一章涉及的关键类和签名

小林把这一章用到的"家伙"整理成了一张签名卡（实测签名）：

**① `ChatAnthropic`——智谱/Claude 的聊天模型类（langchain\_anthropic 包）**

```python theme={null}
from langchain_anthropic import ChatAnthropic

ChatAnthropic(
    model: str,                    # 模型名，如 "glm-4.7" / "claude-sonnet-4-5"
    base_url: str | None = None,   # 平台地址；⚠️ 不带 /v1（会自动拼 /v1/messages）
    api_key: str | None = None,    # 密钥（建议从 os.environ 读，别写死）
    temperature: float = ...,      # 采样温度 0~1
    max_tokens: int = ...,         # 最大输出 token 数（Anthropic 协议必填）
    **kwargs,                      # 其他参数透传给底层
)
```

**关键方法（继承自 langchain\_core 的 `BaseChatModel`）：**

```python theme={null}
model.invoke(input)                    # 单轮调用 → 返回 AIMessage
model.stream(input)                    # 流式调用 → 返回 AIMessageChunk 迭代器
model.bind_tools(tools)                # 绑定工具（第 08 篇）
model.with_structured_output(schema)   # 结构化输出（第 05 篇）
model.batch(inputs)                    # 批量调用 → 返回 list[AIMessage]
model.generate(messages)               # 底层批量接口 → 返回 LLMResult
```

**② `BaseChatModel`——所有聊天模型的地基（langchain\_core.language\_models）**

`ChatAnthropic`、`ChatDeepSeek`、`ChatOpenAI` 全部继承它。你调的 `invoke()`/`stream()` 接口都是它定义的——**这就是"统一插口"的物理位置**。

```python theme={null}
from langchain_core.language_models import BaseChatModel
# 判断一个类是不是"统一插口"的孩子：
assert isinstance(model, BaseChatModel)   # True
```

**③ 三种消息类——invoke 的输入/输出（langchain\_core.messages）**

```python theme={null}
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage

SystemMessage(content="...")   # 系统消息：设定人设
HumanMessage(content="...")    # 用户消息：你说的话
AIMessage(content="...")       # AI 消息：模型返回（resp 的类型）
```

**④ AIMessage 的完整字段（`model_dump()` 后）——你 `show_full()` 打印的东西：**

```python theme={null}
AIMessage(
    content=...,               # 文本（str）或内容块（list[dict]）
    additional_kwargs=...,     # 协议私有的额外信息
    response_metadata=...,     # 协议元信息：id/model/stop_reason/usage
    type="ai",                 # 消息类型
    name=...,                  # 可选名字
    id=...,                    # 消息唯一 ID
    tool_calls=[],             # 工具调用（第 08 篇）
    invalid_tool_calls=[],     # 解析失败的工具调用
    usage_metadata=...,        # token 用量：input/output/total
)
```

> 想验证签名吗？在 Python 里跑 `help(ChatAnthropic)` 或 `inspect.signature(ChatAnthropic)`，能看到完整参数列表。`from langchain_core.language_models import BaseChatModel; isinstance(model, BaseChatModel)` 返回 True，证明"统一插口"真实存在。

## 第四幕：边界——"统一"不统一什么

小林用了一周，慢慢摸清了这套"统一接口"的边界。**invoke() 是统一的，但模型能力不是统一的**：

| 能力                 | 智谱（Anthropic 协议）  | DeepSeek（OpenAI 兼容）   |
| ------------------ | ----------------- | --------------------- |
| 对话 invoke()        | ✅ 统一              | ✅ 统一                  |
| 流式 stream()        | ✅ 统一              | ✅ 统一                  |
| 工具调用 bind\_tools() | ✅ 统一              | ✅ 统一                  |
| **看到思考过程**         | ✅ `thinking` 块直接读 | ❌ 只能看到 token 计数，看不到内容 |

**反例来了**：小林想让两个模型都"打开思考模式"，结果 DeepSeek 那边怎么都读不到思考文本——不是他代码写错了，是**OpenAI 兼容协议本身就不标准地返回思考内容**。他想看思考过程，只有智谱这种 Anthropic 兼容协议行。**统一接口统一不了"供应商根本没给你的能力"。**

> 想验证这个边界吗？在 `06_thinking.py` 里用智谱跑，能看到思考块；换成 ChatDeepSeek 跑同样的问题——思考内容消失了，只剩 usage 里的 reasoning\_tokens 计数。

## 结论（小林用一整周换来的）

**LangChain 把"跟不同大模型说话"统一成了一个插口：`model.invoke()` 进，`resp.content` 出。** 换模型只改三行（包名、模型名、base\_url），因为所有主流平台底层就两套协议（OpenAI 兼容 / Anthropic 兼容），LangChain 的协议适配层把它们都翻译成了同一个 `BaseChatModel` 接口。**但"统一接口"只统一了"怎么调"，统一不了"模型有什么能力"**——供应商没给的能力（比如某些协议看不到思考内容），LangChain 也变不出来。

## 复现信号：什么时候你会想起这一章

以后你写任何调大模型的代码，看到下面任何一个信号，就该想起"统一插口"：

1. **你准备手写 requests 调大模型 API**——停一下。先问自己：这个项目以后会不会换模型供应商？会，就用 LangChain。
2. **报错里出现 `/v1` 或 `base_url`**——想起小林的第一个坑：ChatAnthropic 自动拼 `/v1`（base\_url 别带），ChatOpenAI 不拼（base\_url 要带）。
3. **你想给模型加"能力"**——想清楚这是协议支持的（流式/工具调用）还是供应商独家的（思考内容）。统一接口帮不了你变出没有的能力。

想继续看小林的故事吗？他学会了"统一插口"之后，第一个困惑是：**"我用 `show_full()` 打印模型返回的东西，里面那堆看不懂的字段——response\_metadata、usage\_metadata、tool\_calls——到底都是干嘛的？"** 他决定把模型返回的"信封"拆开看看——[翻到第 2 篇：AIMessage 与三协议](/doc/doc/narrative-course/02-AIMessage与三协议)。
