# deepseek-recipe 代码调研：DeepSeek 推理服务的 Prompt 拼装（PE）

> **源码快照**：`deepseek-recipe` `935817c`（v0.2.0，2026-10-20），DeepInfer 同期开源。  
> **结论先行**：DeepSeek 不是用 Jinja 渲染 `chat_template.jinja`，而是用 Rust 代码把 Chat Completions / Responses / Messages 统一成 `Conversation`，再由 `DeepseekV4Encoding` / `DeepseekV41Encoding` 直接拼 prompt，并把“特殊控制 token”和“普通文本”在类型层分开。  
> 本文只讨论 prompt 侧（PE），不展开 DeepInfer 的算子/调度。

---

## 0. 三个东西不要混

| 组件 | 角色 | 是否包含 HTTP 服务 |
|---|---|---|
| **DeepInfer** | 推理引擎：V4.1-Flash 的 kernel、并行、投机解码、SWA replay、结构化输出等 | 否；开源版是 offline inference，README 明确 KV cache、PD 分离、负载均衡等“完整服务”组件不在本次 release |
| **deepseek-recipe** | 协议/PE 网关库：请求格式转换 → Conversation → prompt/token → 输出解析 | 否；仓库带 `server-rs` / `server-py` 示例，但推理是 mock |
| **server-rs / server-py** | 示例网关：暴露 `/v1/chat/completions`、`/v1/responses`、`/v1/messages` | 是，但只用于演示；生产要把 mock 换成真实 backend |

端到端数据流：

```text
HTTP request (Chat / Responses / Messages)
  → protocol adapter（校验 + 转成 Conversation）
  → DeepseekV4Encoding / DeepseekV41Encoding
  → RenderedPrompt { prompt, special_ranges, image_sources }
  → PromptTokenizer（特殊 token 按 ID，普通文本按字符编码）
  → prompt_token_ids + multi_modal_data
  → DeepInfer / 任意 backend
  → StreamProcessor（按同一套 special tokens 解析）
  → 原协议响应
```

所以“模型网关”不是一个单体，而是 **DeepInfer（算）+ deepseek-recipe（协议/PE）+ 你的 HTTP/工具执行/调度** 三层。`deepseek-recipe` 的 README 也明确：调用方必须提供模型推理、工具执行和 HTTP transport。

---

## 1. 为什么说它“替换了 Jinja”

传统 HF/vLLM 服务通常用 `chat_template.jinja` 把 messages 渲染成一个字符串。`deepseek-recipe` 里没有 Jinja，也没有 `chat_template.jinja` 路径；PE 由 Rust 代码生成。核心差异不是“换了一个模板引擎”，而是：

1. **控制 token 与普通文本类型分离**：`Prompt` 同时保存 `text` 和 `special_ranges`；只有通过 `Prompt::push_special` / `Special` 片段写入的才是控制 token，用户消息、工具结果、工具名、schema 都是 plain text。
2. **tokenizer 按 range 编码**：特殊 range 查 added vocabulary 得到特殊 token ID；普通 region 用“去掉 added vocabulary 的 tokenizer”编码。因此用户在消息里手写 `<｜User｜>`，只会被编码成这些字符的普通 token，不会变成控制 token。Jinja 直接拼字符串做不到这一点。
3. **模型版本分支在代码里**：V4 与 V4.1 的 system 处理、thinking marker、tool 格式、effort prompt 都由两套 encoder 写死；不会出现模板漂移（template drift）。
4. **性能/依赖**：Rust 渲染，无 Python/Jinja 运行时；prompt 与 token IDs 一次生成。

对应代码：

- `Prompt` / `Special`：`deepseek-recipe-core/src/prompt.rs`
- `prompt!` 宏：`deepseek-recipe-macros/src/prompt.rs`
- `special_ranges` 编码：`deepseek-recipe-encoding/src/tokenizer.rs`
- 两套 encoder：`deepseek-recipe-encoding/src/v4/dsv4.rs`、`dsv41.rs`

> 代价：自定义模板能力弱；新模型/新格式要改代码发版，而不是改一个 Jinja 文件。

---

## 2. 核心数据结构

### 2.1 `Conversation`

`Conversation` 是协议无关的中间表示（`deepseek-recipe-core/src/conversation.rs`）：

```rust
pub struct Conversation {
    pub messages: Vec<InputMessage>,
    pub thinking_mode: bool,          // 默认 true
    pub tools: Vec<ToolDefinition>,
    pub tool_choice: ToolChoice,      // Auto / None / Required
    pub reasoning_effort: Option<ReasoningEffort>, // Low / High / Xhigh / Max
    pub response_format: ResponseFormat, // Text / JsonObject / JsonSchema / Regex
}
```

`InputMessage` 只有五种（`messages.rs`）：

```rust
enum InputMessage {
    System { content: Prompt, image_sources: Vec<ImageSource>, tools: Option<Vec<ToolDefinition>> },
    User { content: Prompt, image_sources: Vec<ImageSource> },
    Assistant { content: String, reasoning_content: Option<String>,
                tool_calls: Option<Vec<ToolCall>>, prefix: bool },
    Tool { content: Prompt, image_sources: Vec<ImageSource>, tool_call_id: Option<String> },
    LatestReminder { content: String },
}
```

注意：

- `System`、`User`、`Tool` 的 content 是 `Prompt`，可以带显式图片占位符；
- `Assistant` 的 content / reasoning 是普通 `String`，只有 grounding token（`<｜ref｜>` 等）会被识别为特殊 token；
- `Tool` 与 `Assistant.tool_calls` 分开存，渲染时再拼成 `<tool_result>` / DSML block。

### 2.2 `Prompt` 与 `SpecialToken`

`Prompt` 是“文本 + 特殊 token 的字节区间”：

```rust
pub struct Prompt {
    text: String,
    special_ranges: Vec<Range<usize>>,
}
```

特殊 token 的拼写（V4/V4.1 默认，`special_tokens.rs`）：

| 用途 | 默认拼写 |
|---|---|
| BOS | `<｜begin▁of▁sentence｜>` |
| EOS | `<｜end▁of▁sentence｜>` |
| System | `<｜System｜>` |
| User | `<｜User｜>` |
| Assistant | `<｜Assistant｜>` |
| Thinking start / end | `<think>` / `</think>` |
| Latest reminder | `<｜latest_reminder｜>` |
| DSML | `｜DSML｜` |
| Image | `<｜image｜>` |
| Grounding | `<｜ref｜>`、`<｜box｜>`、`<｜point｜>`、`<｜polygon｜>` 及闭合形式 |

`prompt!` 宏在编译期把格式串展开成 `push_plain` / `Special` 调用，行为类似 `format!`，但类型是 `Prompt`，不是 `String`。

### 2.3 协议适配器

三个协议都实现 `ProtocolRequest`，最终产出 `ConversationRequest`：

| 协议 | 输入 | 关键转换 |
|---|---|---|
| **Chat Completions** | `messages[]`、`tools`、`response_format`、`prefix` | `system` → System；assistant `tool_calls` → Assistant + Tool；`prefix:true` 只允许最后一条 assistant |
| **Responses** | `input[]` Items、`instructions`、`tools`、`text.format` | `message`/`function_call`/`function_call_output`/`reasoning` → 对应消息；`developer` → System |
| **Anthropic Messages** | 顶层 `system`、content blocks、`thinking`、`tool_use/tool_result` | `system` → System；`thinking` blocks 拼成 reasoning_content；`tool_use` → ToolCall；`tool_result` → Tool；`system` role 在 V4.1 保留为 System，在 V4 变成 `<system-reminder>` user |

`resolve_thinking_mode()` 决定 thinking 与 effort（`protocol/reasoning.rs`）：

- 显式 `thinking` 优先；
- 否则 `effort:"none"` 关闭，其他 effort 开启；
- 都没有则用 `default_thinking_mode`（默认 **true**）；
- 开启时 effort 缺省为 **High**；
- 映射：`minimal/low → Low`，`medium/high → High`，`xhigh → Xhigh`，`ultra/max → Max`。

也就是说：**PE 的“是否思考、思考多深”在进入 encoder 前就已经变成 `thinking_mode + reasoning_effort` 两个字段**，encoder 只负责按模型版本渲染。

---

## 3. V4.1 的 PE 拼装顺序（当前主线）

`DeepseekV41Encoding::render_conversation()`（`dsv41.rs:250`）的顺序可以概括为：

> 值得注意的是，recipe **不额外注入“你是 DeepSeek”之类的身份 system prompt**；它只注入三类系统文本：reasoning effort、tool prompt、response format。API 传入的 system 内容原样保留（Anthropic 的 `x-anthropic-billing-header` 会被剥掉）。

```text
BOS
if thinking_mode:
    <｜System｜> + "Reasoning Effort: 75 (range 1-100, ...)\n\n"
for each normalized message:
    System:
        if index > 0: <｜System｜>
        content
        if message.tools: "\n\n### Updated Tool Schemas\n\n" + schemas
    User:
        if previous is User/Tool: "\n\n"
        else: <｜User｜>
        content
    Assistant:
        <｜Assistant｜> + thinking_marker
        if reasoning should render: reasoning_content
        content
        DSML tool calls
        if not prefix: <｜end▁of▁sentence｜>
    Tool:
        if previous is Tool: "\n\n"
        else: <｜User｜>
        "<tool_result>" + content + "</tool_result>"
    LatestReminder:
        <｜latest_reminder｜> + content
if not prefix:
    <｜Assistant｜> + (thinking ? "<think>" : "</think>")
if tool_choice == Required:
    "\n\n<｜DSML｜ calls>\n"
```

几个容易忽略的细节：

### 3.1 第一条消息与 System token

- **thinking 模式**：即使第一条是 user，也会先写 `<｜System｜>`，因为 reasoning effort prompt 需要挂在 system 位置。例如无 system 的 thinking 请求开头是：

  ```text
  <｜begin▁of▁sentence｜><｜System｜>Reasoning Effort: 75 (range 1-100, ...)

  <｜User｜>...
  ```

- **非 thinking 模式**：只有第一条是 System 时才写 `<｜System｜>`；否则直接从 `<｜User｜>` 开始（BOS 之后）。但如果有 tool prompt 或 response format，而首条不是 system，V4.1 会先插入一个空 system，于是开头仍会出现 `<｜System｜>`。

### 3.2 System 消息不再“全部搬到开头”

V4.1 保留 system 在原来的位置；后续 system 消息会再写一次 `<｜System｜>`。这正是它支持 mid-conversation system / tool updates 的基础：

- 普通 system：`<｜System｜>content`
- 带工具更新的 system：content 后追加 `### Updated Tool Schemas` 和新的 schema
- 这也意味着 V4.1 的 system 顺序会影响 prefix cache，不能随意重排。

### 3.3 连续 User / Tool 的合并规则

- 连续 user：不在第二条前写 `<｜User｜>`，而是用 `\n\n` 连接；
- tool 紧跟 tool：同样用 `\n\n` 连接，不重复 `<｜User｜>`；
- tool 紧跟 user：tool 结果先渲染，然后 user 用 `\n\n` 连接（不再写 `<｜User｜>`）。

这是为了匹配训练时的“同一轮多块内容”格式。

### 3.4 Assistant 的历史 reasoning 是“有条件渲染”

`drop_thinking = !renders_tool_prompt`：

- **没有工具**：历史 assistant 的 reasoning_content 不渲染，只渲染 content；
- **有工具**：历史 reasoning 会渲染，以保持工具循环的推理连续性；
- **prefix 续写**：始终渲染 reasoning（因为是当前 assistant turn 的一部分）。

这个规则对 token 成本和 agent 质量都很关键：普通聊天不背历史 thinking，工具 agent 才保留。

### 3.5 最终 Assistant marker

- thinking：`<｜Assistant｜><think>`
- 非 thinking：`<｜Assistant｜></think>`
- prefix continuation：不加最终 marker，直接续写已有 assistant 内容。

### 3.6 `tool_choice=required`

如果请求要求必须调用工具，prompt 末尾额外加：

```text
\n\n<｜DSML｜ calls>\n
```

让模型从 tool-call block 内部开始生成。非 required 时模型可以正常回答或调用。

---

## 4. 一个最小例子

### 4.1 非 thinking，只有 user

```text
<｜begin▁of▁sentence｜><｜User｜>a<｜Assistant｜></think>
```

这是测试里的精确期望（`v4/mod.rs` 的 `json_schema...` 测试等）。

### 4.2 thinking，只有 user

```text
<｜begin▁of▁sentence｜><｜System｜>Reasoning Effort: 75 (range 1-100, the higher the value, the more thorough the reasoning)

<｜User｜>a<｜Assistant｜><think>
```

### 4.3 thinking，有 system

```text
<｜begin▁of▁sentence｜><｜System｜>Reasoning Effort: 75 (...)

sys<｜User｜>hi<｜Assistant｜><think>
```

注意 V4.1 不写第二个 `<｜System｜>`：reasoning effort prompt 和第一条 system 共用同一个 System token。

### 4.4 JSON Schema

`response_format: json_schema` 会追加到第一条 system 内容后面：

```text
<｜begin▁of▁sentence｜><｜System｜>

## Response Format:

You MUST strictly adhere to the following schema to reply:
{"type": "object"}<｜User｜>a<｜Assistant｜></think>
```

`json_object` 在 V4.1 里用 `{"type": "json_object"}` 作为 schema；V4 则用一段固定英文 system prompt（见下）。

---

## 5. V4 与 V4.1 的关键差异

两套 encoder 不是简单参数化，而是两套训练格式。

| 维度 | V4 (`DeepseekV4Encoding`) | V4.1 (`DeepseekV41Encoding`) |
|---|---|---|
| System 消息位置 | 全部 hoist 到开头，空 system 丢弃 | 保留原位，支持 mid-conversation system |
| System token | **从不写** `<｜System｜>` | thinking 或首条 system 时写 |
| Reasoning effort | 英文长句：High/Max 两档；Low 不写 | 数字：Low=50，High/Xhigh/默认=75，Max=100 |
| Tool prompt | 作为**额外 system 消息**插入 leading system 之后 | 追加到**第一条 system**内容之后 |
| Tool schema JSON | `serde_json` 带空格风格 | Python `json.dumps` 风格（含 float 指数格式） |
| DSML tag | `<｜DSML｜tool_calls>` / `invoke` / `parameter` | `<｜DSML｜ calls>` / ` invoke` / ` parameter`（注意前导空格） |
| Tool call 排序 | 按 tool call ID；tool 结果也按 ID | 按前一条 assistant 的 call 顺序 |
| 历史 reasoning | 有 tool prompt 才渲染 | 同样有 tool prompt 才渲染；prefix 时总渲染 |
| 图片 | 不支持；替换成 `[Unsupported Image]` | 支持；OpenCV 预处理 |
| 连续 user | 合并（系统 hoist 后） | 合并 |
| 结构化输出 | 单独 system 消息 | 追加到第一条 system |
| 推荐场景 | V4-Pro | V4.1-Flash（当前主线） |

### 5.1 V4 的 reasoning effort prompt

V4 在 thinking 模式下、BOS 之后直接插入一段长英文（不是 system message）：

- `None/High/Xhigh`：`Reasoning Effort: Absolute maximum with no shortcuts permitted. ...`
- `Max`：`Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising. ...`
- `Low`：不插入。

所以 V4 的 prompt 开头是 `BOS + 长英文 + system 内容 + <User>...`，没有 `<｜System｜>`。

### 5.2 V4.1 的 DSML 格式

V4.1 的 tool call block 长这样（注意 tag 名里的空格）：

```text
<｜DSML｜ calls>
<｜DSML｜ invoke name="read_file">
<｜DSML｜ parameter name="path" string="true">a.py</｜DSML｜ parameter>
</｜DSML｜ invoke>
</｜DSML｜ calls>
```

V4 则是：

```text
<｜DSML｜tool_calls>
<｜DSML｜invoke name="read_file">
<｜DSML｜parameter name="path" string="true">a.py</｜DSML｜parameter>
</｜DSML｜invoke>
</｜DSML｜tool_calls>
```

这个差异来自 `dsv41.rs` 的常量 `" calls"`、`" invoke"`、`" parameter"`。看起来像 bug，但它是训练格式的一部分，不能“顺手修正”。

---

## 6. 工具调用的 PE

### 6.1 Tool prompt

V4.1 的 tool prompt 由三部分组成：

```text
## Tools

You have access to a set of tools...
<｜DSML｜ calls>
<｜DSML｜ invoke name="$TOOL_NAME">
<｜DSML｜ parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</｜DSML｜ parameter>
...
</｜DSML｜ invoke>
</｜DSML｜ calls>

String parameters should be specified as is and set `string="true"`. For all other types..., pass the value in JSON format and set `string="false"`.

If thinking_mode is enabled..., you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls...

### Available Tool Schemas
{每个 tool 的 JSON，一行一个}
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
```

V4 的版本几乎相同，但 tag 不带前导空格，schema 用 `stringify_spaced`，并作为独立 system message 插入。

### 6.2 Tool schema 里没有 `strict`

`tool_definition_json()` 只输出：

```json
{"name": "...", "description": "...", "parameters": {...}}
```

`strict` 不进入 prompt；它只进入 `OutputConstraint::ToolCalls` 的约束 schema（供 backend 做 constrained decoding）。也就是说：**prompt 负责“告诉模型 schema”，strict 的强制由解码侧负责**。

### 6.3 Tool result

两种模型都用：

```text
<｜User｜><tool_result>{content}</tool_result>
```

连续 tool result 之间用 `\n\n` 分隔，不重复 `<｜User｜>`。

### 6.4 工具更新 / 延迟加载

- `ToolDefinition.defer_loading = true` 的工具不出现在初始 tool prompt；
- V4.1 的 `InputMessage::System.tools` 会在 system 内容后追加 `### Updated Tool Schemas`；
- V4 忽略 `System.tools`；
- 这对应 Messages 的 `tool_addition` / `tool_removal`、Responses 的 tool search 等协议能力。

---

## 7. Thinking / Prefix / 采样参数

### 7.1 Thinking

- 默认开启（`Conversation::default().thinking_mode = true`）；
- 最终 marker：`<think>`（开）或 `</think>`（关）；
- 历史 reasoning 只在“有工具”或“prefix 续写”时渲染；
- reasoning content 里的 grounding token（`<｜ref｜>` 等）会被识别为显式特殊 token，其余是普通文本。

### 7.2 Prefix continuation

Chat Completions 的 `prefix:true` 只允许出现在最后一条 assistant：

- 不写最终 `<｜Assistant｜><think>`；
- 不写 EOS（`!prefix` 才写）；
- 如果 prefix 的 content 为空且 reasoning 非空，V4.1 不写 `</think>`，让模型继续 reasoning；
- 如果 prefix 已有 content，则写 `</think>` 后继续 content。

这是 Chat Completions 的 FIM/续写能力在 DeepSeek PE 上的落点。

### 7.3 采样参数也被 recipe 归一化

虽然不属于 PE 文本，但属于网关行为：

- thinking 模式 `temperature = 1.0`；
- `top_p`：V4.1 thinking 至少 0.95；V4 thinking 有工具 0.95、无工具 1.0；非 thinking 按请求/1.0；
- `max_tokens` 默认：非 thinking 8192，thinking 65536，thinking+max effort 131072；再按 prompt 长度和 context 上限裁剪（`resolve_max_tokens`）；
- `max_tokens` 上限默认 393216，可按模型/部署配置。

---

## 8. 结构化输出

| 协议 | 输入 | PE 行为 |
|---|---|---|
| Chat Completions | `response_format: json_object` | 要求 prompt 里包含 `json` 字样，否则 400；V4 注入固定 JSON system prompt，V4.1 注入 `{"type":"json_object"}` schema |
| Chat Completions | `json_schema` | 校验/标准化 schema；V4 作为独立 system message，V4.1 追加到第一条 system |
| Chat Completions | `regex` | **不写入 prompt**；只作为 `ResponseFormat::Regex` / `OutputConstraint::Regex` 交给 backend |
| Responses | `text.format: json_schema` | 同上，渲染 schema |
| 工具调用 | `tools` + tool choice | 生成 `OutputConstraint::ToolCalls`；`required` 时 prompt 末尾打开 tool-call block |

注意：渲染 schema ≠ 强制 schema。Recipe 只负责把 schema 写进 prompt，并给 backend 一个 `OutputConstraint`；是否用 constrained decoding 由调用方决定。DeepInfer 的 `SamplingParams(so_args=...)` 就是接这个约束的地方，但需要你自己做 `OutputConstraint → so_args` 的映射。

---

## 9. 图片

- 图片在消息里被替换成显式 `<｜image｜>` 特殊 token，`ImageSource` 按出现顺序记录在 `RenderedPrompt.image_sources`；
- V4 不支持图片：转换时替换成 `[Unsupported Image]`，不校验图片；
- V4.1 支持 system/user/tool 图片；assistant 图片报错；
- 图片 token：`IMAGE_SPECIAL_TOKEN_ID = 129264`；prompt 里只有 1 个占位 token，实际图片 token 由 `MultiModalData::image_token_adjustment()` 计算并加到 context 用量；
- 图片预处理在 `deepseek-recipe-image`（OpenCV），不在 PE 核心；但它会影响最终 token 预算。

---

## 10. Tokenizer：PE 精确性的最后一步

`PromptTokenizer` 做两件事：

1. 克隆一个去掉 added vocabulary 的 tokenizer，用于编码普通文本；
2. 保留 added vocabulary 的 spelling→ID，用于编码 `special_ranges`。

结果是：

- 用户消息里写 `<｜User｜>` → 按字符编码，不是控制 token；
- 模板插入的 `<｜User｜>` → 直接是特殊 token ID；
- 不会额外加 BOS（BOS 是模板显式写的）；
- plain region 批量编码，可并行。

这也是为什么 `RenderedPrompt` 必须同时返回 `prompt` 和 `special_ranges`：只拿字符串去 tokenize 会丢掉“哪些是控制 token”的信息。

---

## 11. 输出侧：同一套 token 语义

`StreamProcessor` + `OutputParser` 用同一套 `SpecialTokens` 解析输出：

- 只有“特殊 token 段”才匹配 `</think>` / DSML 标记；普通 token 拼出的同样字符串只是内容；
- 解析 reasoning、DSML tool calls、JSON、stop sequence；
- tool call 的 `string="true|false"` 被还原成 JSON 对象；
- `ChatCompletion` / `Responses` / `Messages` 各自的 chunk generator 再把内部事件转成对应协议的 SSE。

这形成了 PE 与解析的对称性：**prompt 里怎么写的控制 token，输出就怎么按 token 识别**。

---

## 12. 和 DeepInfer 怎么接

`server-rs` 的 `prepare_processor()` 展示了最小接线：

```rust
let rendered = match request.model_version {
    ModelVersion::DeepseekV4Pro => DeepseekV4Encoding::new().render_conversation(&request.conversation),
    _ => DeepseekV41Encoding::new().render_conversation(&request.conversation),
};
let multi_modal_data = resolver.resolve(&rendered.image_sources, &mut quota).await?;
let generator = T::chunk_generator(&request, ...);
StreamProcessor::new(generator, request.parsing_options)
```

生产环境把 mock inference 替换成 DeepInfer：

- 传 `prompt` 或 `prompt_token_ids`（推荐后者，避免二次 tokenize）；
- 传 `multi_modal_data`；
- 传 `SamplingParams`（temperature/top_p/max_tokens/seed/结构化输出约束）；
- 把 DeepInfer 的 token 流喂给 `StreamProcessor`。

DeepInfer 的 offline example 本身接受 `prompts` 或 `prompt_token_ids` + `multi_modal_data`，正好对应 recipe 的输出。README 也说明开源版不包含完整 serving 组件（KV cache、PD 分离、负载均衡），所以“网关”仍需自己补 HTTP、鉴权、路由、工具执行。

---

## 13. 评价

**做得好的地方**

- **PE 与官方 API 对齐**：三个协议入口最终走同一套 `Conversation` 和 encoder，减少“自建网关和官方 API 漂移”。
- **特殊 token 安全**：类型层分离 + range 编码，避免用户文本注入控制 token；这是 Jinja 字符串模板最难保证的。
- **模型版本精确**：V4/V4.1 的差异（system、effort、DSML、图片）都显式编码，不靠模板 if/else。
- **可测试**：encoder 有大量精确字符串断言，PE 回归测试容易做。
- **Rust + Python bindings**：性能好，易嵌入不同网关。

**局限**

- 只覆盖 DeepSeek V4 / V4.1；新模型要加 encoder。
- 不支持自定义 chat template；灵活性低于 Jinja。
- `strict` 不进入 prompt，强制 schema 仍依赖 backend 的 constrained decoding。
- 开源 DeepInfer 是 offline release，不是完整 serving；recipe 的 server 示例是 mock。
- 图片 V4 不支持；V4.1 依赖 OpenCV 预处理。
- 工具执行、HTTP、鉴权、限流、KV cache/PD 分离都要自己接。

---

## 14. 参考

源码（快照 `935817c`）：

- [deepseek-recipe 仓库](https://github.com/deepseek-ai/deepseek-recipe)
- [V4.1 encoder](https://github.com/deepseek-ai/deepseek-recipe/blob/935817c66f5d6e64838f419692545f06787e485f/deepseek-recipe-encoding/src/v4/dsv41.rs)
- [V4 encoder](https://github.com/deepseek-ai/deepseek-recipe/blob/935817c66f5d6e64838f419692545f06787e485f/deepseek-recipe-encoding/src/v4/dsv4.rs)
- [Prompt / SpecialToken](https://github.com/deepseek-ai/deepseek-recipe/blob/935817c66f5d6e64838f419692545f06787e485f/deepseek-recipe-core/src/prompt.rs)
- [PromptTokenizer](https://github.com/deepseek-ai/deepseek-recipe/blob/935817c66f5d6e64838f419692545f06787e485f/deepseek-recipe-encoding/src/tokenizer.rs)
- [server-rs 示例](https://github.com/deepseek-ai/deepseek-recipe/blob/935817c66f5d6e64838f419692545f06787e485f/server-rs/src/lib.rs)

配套：

- [DeepInfer](https://github.com/deepseek-ai/DeepInfer)
- [DeepInfer offline example](https://github.com/deepseek-ai/DeepInfer/blob/main/examples/offline_inference.py)
- [DeepInfer structured output example](https://github.com/deepseek-ai/DeepInfer/blob/main/examples/_structured_output.py)
