> ## 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：从一次"朴素 API 调用"开始

# 01 · 为什么需要 LangChain：从一次"朴素 API 调用"开始

> **本片目标**：用一个朴素 `requests` 调智谱 API 的例子，展示"没有 LangChain 的世界"，再展示 LangChain 的 ChatModel 如何统一一切，并回答：**LangChain 到底多做了哪几件事？**
> **新增规定性：1**（此后你调模型只有一个入口：`chat_model.invoke(...)`）
> **数据字典**：ChatAnthropic 构造参数、AIMessage 骨架。
> **进程线程模型**：同步 `invoke` 在当前线程阻塞，直到响应返回。
> **网络模型**：一次 HTTPS POST 到 `/v1/messages`，JSON 请求体、JSON 响应体。

***

## 1. 上集回顾

第 00 篇我们有了环境和 API key。现在问题来了：

> **为什么我不直接 `requests.post` 调智谱的 API，而要学一个叫 LangChain 的框架？**

这个问题必须在第 01 篇回答。因为如果 LangChain 只是"给 requests 套了层皮"，那它不值得学——你用 `requests` 就够了。**理解 LangChain 的价值 = 理解它解决了什么必须解决的问题。**

***

## 2. 朴素世界：直接调智谱 API（Anthropic 协议）

你打开智谱的文档，看到它的接口长这样（Anthropic Messages API）：

```
POST https://open.bigmodel.cn/api/anthropic/v1/messages
Headers: x-api-key: <key>   content-type: application/json   anthropic-version: 2023-06-01
Body: {
  "model": "glm-4.7",
  "max_tokens": 512,
  "messages": [
    {"role": "system", "content": "你是助手"},
    {"role": "user",   "content": "你好"}
  ]
}
```

你用 `requests` 写出来是这样的（`code/01_first_chat.py` 里的 `raw_api_call()` 函数）：

```python theme={null}
import requests

resp = requests.post(
    f"{BASE_URL}/v1/messages",
    headers={
        "x-api-key": API_KEY,
        "content-type": "application/json",
        "anthropic-version": "2023-06-01",
    },
    json={
        "model": "glm-4.7",
        "max_tokens": 512,
        "messages": [
            {"role": "system", "content": "你是助手"},
            {"role": "user",   "content": "你好"},
        ],
    },
)
data = resp.json()
answer = data["content"][0]["text"]   # ← 关键！Anthropic 的 content 是列表，取 [0]["text"]
```

**这段代码能跑，但它藏着 4 个问题**：

1. **协议细节写死在代码里**——`anthropic-version` 头、`data["content"][0]["text"]` 这种取数逻辑，全是你搜文档搜来的。换一家模型（比如 DeepSeek，OpenAI 协议），响应结构变成 `data["choices"][0]["message"]["content"]`，这段代码**全部要重写**。
2. **消息序列化是你自己做的**——`{"role": "system", "content": ...}` 这个字典结构，你得自己记住"system 对应什么、user 对应什么"。
3. **错误处理、重试、超时、流式**——全都没有。生产环境第一个月就会因为一次 429 超限而崩。
4. **没法组合**——你后面要做的 RAG（检索→拼上下文→生成）是**多个环节的组合**。用 requests 写，每个环节都要自己管理"输入从哪来、输出去哪"。

***

## 3. LangChain 的做法：一个接口，调用所有模型

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

model = ChatAnthropic(
    model=os.environ.get("ZHIPU_MODEL", "glm-4.7"),
    base_url=os.environ.get("ZHIPU_BASE_URL", "https://open.bigmodel.cn/api/anthropic"),
    api_key=os.environ["ZHIPU_API_KEY"],
    max_tokens=512,
)

answer = model.invoke("你好")   # ← 就这一行，后面全交给框架
```

对比朴素版，LangChain **替你做了 4 件事**：

| # | 朴素 requests 版                               | LangChain 版                        |
| - | ------------------------------------------- | ---------------------------------- |
| 1 | 自己拼 headers（x-api-key / anthropic-version）  | 构造 `ChatAnthropic` 时传入，框架替你加       |
| 2 | 自己把字符串包成 `{"role": "user", "content": ...}` | 框架自己构造消息请求体                        |
| 3 | 自己解析响应 `data["content"][0]["text"]`         | 返回统一对象 `AIMessage`，`.content` 就是答案 |
| 4 | 换厂商全重写                                      | 换厂商只改 import + 构造参数                |

**这就是"统一接口"（规定性 1）**：不管底层是 Anthropic 协议还是 OpenAI 协议，你面对的都是同一个类 `ChatAnthropic`/`ChatOpenAI`，同一个方法 `invoke()`，同一个返回类型 `AIMessage`。

> **逻辑必然性**：只要你有"换模型厂商不用重写业务代码"的诉求，就必然需要一个"统一信封"——把各家协议的差异全部吸收掉。这就是 `langchain-core` 存在的第一理由。

***

## 4. 数据字典：ChatAnthropic（关键类的数据）

### 4.1 `ChatAnthropic` 构造参数（langchain-anthropic）

| 参数            | 数据类型         | 必需    | 说明                                                     |
| ------------- | ------------ | ----- | ------------------------------------------------------ |
| `model`       | `str`        | ✅     | 模型名，如 `glm-4.7`、`deepseek-chat`                        |
| `base_url`    | `str`        | ✅（智谱） | API 端点。**Anthropic 协议自动拼 `/v1/messages`，所以这里不带 `/v1`** |
| `api_key`     | `str`        | ✅     | 密钥，从 `.env` 读                                          |
| `max_tokens`  | `int`        | 建议    | 最大输出 token 数                                           |
| `temperature` | `float`      | 可选    | 采样温度（0\~2）                                             |
| `timeout`     | `float/None` | 可选    | 请求超时秒数                                                 |
| `max_retries` | `int`        | 可选    | 失败重试次数（默认 2）                                           |

### 4.2 `invoke()` 返回：`AIMessage` 骨架（完整 9 字段在第 02 篇）

```python theme={null}
AIMessage(
    content="你好！我是Z.ai训练的GLM大语言模型...",  # str，答案本体
    response_metadata={...},  # 见下（实测结构）
    usage_metadata={...},     # input_tokens / output_tokens / total_tokens
    type="ai",
    id="lc_run--xxx",
    tool_calls=[],            # 第 02 篇细讲
    invalid_tool_calls=[],
)
```

**实测 `response_metadata`（智谱 glm-4.7，Anthropic 协议，2026-08 实测）**：

```json theme={null}
{
  "id": "msg_20260805091423...",
  "model": "glm-4.7",
  "stop_reason": "end_turn",
  "usage": {
    "input_tokens": 6, "output_tokens": 45,
    "cache_read_input_tokens": 0, "server_tool_use": {"web_search_requests": 0}
  },
  "model_name": "glm-4.7",
  "model_provider": "anthropic"
}
```

**实测 `usage_metadata`**：

```json theme={null}
{
  "input_tokens": 6, "output_tokens": 45, "total_tokens": 51,
  "input_token_details": {"cache_read": 0}
}
```

> `model_provider: "anthropic"` 是判断协议来源的关键字段——智谱返回 `anthropic`，DeepSeek 返回 `openai`（第 02 篇细讲）。

***

## 5. 关键方法

| 方法        | 签名                                                       | 返回值                               | 用途          |              |
| --------- | -------------------------------------------------------- | --------------------------------- | ----------- | ------------ |
| `invoke`  | \`invoke(messages: str/list\[BaseMessage]                | dict, config=None) -> AIMessage\` | `AIMessage` | 同步单次调用（本片主角） |
| `batch`   | `batch(inputs: list, config=None) -> list[AIMessage]`    | `list[AIMessage]`                 | 批量（第 12 篇）  |              |
| `stream`  | `stream(input, config=None) -> Iterator[AIMessageChunk]` | 迭代器                               | 流式（第 04 篇）  |              |
| `ainvoke` | `ainvoke(input, config=None) -> AIMessage`               | `AIMessage`                       | 异步（第 12 篇）  |              |

> 注意：`invoke` 接受**字符串**（一个 HumanMessage 的快捷方式）、**消息对象列表**或**字典**。本片用字符串；第 02 篇开始用消息对象。

***

## 6. 进程线程模型

```
你（主线程）
  │
  ├─ model.invoke("你好")          ← 同步阻塞
  │     │
  │     └─ 当前线程被占住，等网络响应
  │           （通常 1~3 秒，智谱 glm-4.7 实测 invoke ≈ 1.9s）
  │
  └─ 响应返回 → 继续执行
```

**结论**：同步 `invoke` 是**阻塞调用**——调用期间当前线程什么都不干，就等网络。单线程下，一次问答 = 一次"卡住" 1\~3 秒。生产环境（第 11、12 篇）会学怎么用线程池/异步解决。

***

## 7. 网络模型

```
model.invoke("你好")
  │
  └─ 构造 ChatAnthropic 时内部建了一个 httpx.Client（同步客户端）
       │
       └─ POST https://open.bigmodel.cn/api/anthropic/v1/messages
            Headers: x-api-key / content-type: application/json / anthropic-version
            Body: {"model":"glm-4.7","max_tokens":512,"messages":[{"role":"user","content":"你好"}]}
            │
            └─ 响应: {"content":[{"type":"text","text":"收到..."}], ...}
                 │
                 └─ 框架解析 → AIMessage(content="收到...")
```

**关键点**：

* 传输协议：**HTTPS**，一次 POST 请求。
* 请求体/响应体：**JSON**。
* 请求头 `anthropic-version` 由 SDK 自动带（这是 Anthropic 协议的规定性）。
* 超时、重试（默认 2 次）、429 处理，`ChatAnthropic` 已内置——这是朴素 requests 没有的。

***

## 8. 验证：跑起来

配套代码 `code/01_first_chat.py` 同时包含**朴素 requests 版**和 **LangChain 版**，并打印两者的原始响应让你对比。

```powershell theme={null}
cd enterprise-rag-course\code
python 01_first_chat.py
```

**预期输出（节选）**：

```
========== 朴素版：直接 requests 调 API ==========
响应 JSON 的 content 字段: [{'type': 'text', 'text': '收到，小林工程师。...'}]
答案: 收到，小林工程师。...

========== LangChain 版：ChatAnthropic ==========
AIMessage.content: 收到，小林工程师。...
AIMessage.response_metadata: {'id': 'msg_xxx', 'model': 'glm-4.7', 'stop_reason': 'end_turn', ...}
```

***

## 9. 边界

* **ChatAnthropic 不是唯一入口**——还有 `ChatOpenAI`（DeepSeek/通义/Kimi）。两个类的 `invoke` 行为一致，但构造参数略有差异（OpenAI 协议 base\_url 要带 `/v1`）。
* **别自己 new httpx 调模型 API**——除非你有极特殊的诉求（比如自己控制 HTTP 会话复用）。框架已内置超时/重试/流式。
* **AIMessage 还不是字符串**——`invoke` 返回对象。想直接拿文本要么 `.content`，要么（第 04 篇）接 `StrOutputParser`。

***

## 推荐资料（延伸阅读）

* [LangChain 官方文档 · 总览](https://docs.langchain.com/oss/python/langchain/overview) —— LangChain 到底解决了什么问题（官方口径）
* [LangChain 官方文档 · 模型](https://docs.langchain.com/oss/python/langchain/models) —— ChatModel 概念与统一接口的权威说明
* [LangChain 官方文档 · 组件架构](https://docs.langchain.com/oss/python/langchain/component-architecture) —— 标准接口 + 集成包的架构设计

***

## 10. 未完待续

`model.invoke("你好")` 返回的 `AIMessage` 里有 9 个字段——`content`、`tool_calls`、`usage_metadata`…… 为什么一个"回答"要带这么多东西？**模型到底返回了什么、怎么被表达成统一信封？** 这就是第 02 篇。

→ [02 · 消息类数据字典](/doc/doc/enterprise-rag-course/02-消息类数据字典)
