docs: translate DeepSeek SSE behavior note and raw-sample README to English
This commit is contained in:
@@ -1,98 +1,98 @@
|
||||
# DeepSeek SSE 行为结构说明(第三方逆向版)
|
||||
# DeepSeek SSE behavior notes (third-party reverse-engineered)
|
||||
|
||||
> 说明:本文基于当前仓库 `tests/raw_stream_samples/` 下全部 `upstream.stream.sse` 原始流样本整理而成,属于第三方逆向观察文档,不是官方协议。
|
||||
> 当前 corpus 由 4 份原始流组成,覆盖搜索+引用、风控终态、Markdown 输出和空格敏感输出等行为。
|
||||
> 补充:文末还会注明少量“当前实现已确认、但 corpus 尚未完整覆盖”的行为,例如长思考场景下的自动续写状态。
|
||||
> Note: this document is compiled from all the `upstream.stream.sse` raw stream samples under `tests/raw_stream_samples/` in the current repository. It is a third-party reverse-engineering observation, not an official protocol.
|
||||
> The current corpus consists of 4 raw streams covering behaviors such as search+citations, risk-control terminal state, Markdown output, and whitespace-sensitive output.
|
||||
> Addendum: the end of this document also notes a few behaviors that are "confirmed in the current implementation but not yet fully covered by the corpus", such as automatic continuation state in long-thinking scenarios.
|
||||
|
||||
文档导航:[文档总索引](./README.md) / [测试指南](./TESTING.md) / [样本目录说明](../tests/raw_stream_samples/README.md)
|
||||
Docs: [Docs index](./README.md) / [Testing guide](./TESTING.md) / [Sample directory notes](../tests/raw_stream_samples/README.md)
|
||||
|
||||
## 1. 样本覆盖
|
||||
## 1. Sample coverage
|
||||
|
||||
下列样本共同构成了本文的观察基础:
|
||||
The following samples together form the observational basis of this document:
|
||||
|
||||
| 样本 | 观察重点 |
|
||||
| Sample | Focus |
|
||||
| --- | --- |
|
||||
| [guangzhou-weather-reasoner-search-20260404](../tests/raw_stream_samples/guangzhou-weather-reasoner-search-20260404/upstream.stream.sse) | 搜索+思考流程,包含 `reference:N` 引用标记与工具片段 |
|
||||
| [content-filter-trigger-20260405-jwt3](../tests/raw_stream_samples/content-filter-trigger-20260405-jwt3/upstream.stream.sse) | `CONTENT_FILTER` 终态分支,包含拒答模板与 `ban_regenerate` |
|
||||
| [markdown-format-example-20260405](../tests/raw_stream_samples/markdown-format-example-20260405/upstream.stream.sse) | Markdown 输出的早期样本,用于观察 token 级输出形态 |
|
||||
| [markdown-format-example-20260405-spacefix](../tests/raw_stream_samples/markdown-format-example-20260405-spacefix/upstream.stream.sse) | Markdown 输出修正样本,用于验证空格 chunk 必须保留 |
|
||||
| [guangzhou-weather-reasoner-search-20260404](../tests/raw_stream_samples/guangzhou-weather-reasoner-search-20260404/upstream.stream.sse) | Search + thinking flow, including `reference:N` citation markers and tool fragments |
|
||||
| [content-filter-trigger-20260405-jwt3](../tests/raw_stream_samples/content-filter-trigger-20260405-jwt3/upstream.stream.sse) | `CONTENT_FILTER` terminal branch, including the refusal template and `ban_regenerate` |
|
||||
| [markdown-format-example-20260405](../tests/raw_stream_samples/markdown-format-example-20260405/upstream.stream.sse) | Early Markdown-output sample, used to observe token-level output shape |
|
||||
| [markdown-format-example-20260405-spacefix](../tests/raw_stream_samples/markdown-format-example-20260405-spacefix/upstream.stream.sse) | Markdown-output correction sample, used to verify that whitespace chunks must be preserved |
|
||||
|
||||
当前 corpus 的整体特征是 `message` 帧占绝对多数,控制事件只占很小一部分,但它们决定了流的生命周期和最终状态。
|
||||
The overall characteristic of the current corpus is that `message` frames are by far the majority, and control events make up only a small fraction, but they determine the stream's lifecycle and final state.
|
||||
|
||||
## 2. 总体结构
|
||||
## 2. Overall structure
|
||||
|
||||
DeepSeek 的这类输出可以分成两层看:
|
||||
This kind of DeepSeek output can be viewed in two layers:
|
||||
|
||||
1. SSE 事件层。
|
||||
2. JSON 载荷层。
|
||||
1. The SSE event layer.
|
||||
2. The JSON payload layer.
|
||||
|
||||
事件层负责传输边界,载荷层负责业务状态。实现时不要把 HTTP chunk、SSE block 和业务 JSON 混为一体。
|
||||
The event layer handles transport boundaries; the payload layer handles business state. When implementing, do not conflate HTTP chunks, SSE blocks, and business JSON.
|
||||
|
||||
最常见的时序可以概括为:
|
||||
The most common sequence can be summarized as:
|
||||
|
||||
```text
|
||||
ready
|
||||
update_session
|
||||
message(初始化 envelope)
|
||||
message(正文 / 片段 / 状态增量)
|
||||
message(状态收口)
|
||||
message(initialization envelope)
|
||||
message(body / fragment / state delta)
|
||||
message(state finalization)
|
||||
finish
|
||||
update_session
|
||||
title
|
||||
close
|
||||
```
|
||||
|
||||
`finish` 表示生成流结束,但不是唯一的终止信号;真正的语义终态通常还要结合 `response/status`、`quasi_status` 和 `close` 一起判断。
|
||||
`finish` indicates the generation stream has ended, but it is not the only termination signal; the real semantic terminal state usually has to be judged together with `response/status`, `quasi_status`, and `close`.
|
||||
|
||||
## 3. SSE 事件层
|
||||
## 3. SSE event layer
|
||||
|
||||
当前 corpus 中观察到的事件类型如下:
|
||||
The event types observed in the current corpus are:
|
||||
|
||||
| 事件 | 作用 | 处理建议 |
|
||||
| Event | Purpose | Handling advice |
|
||||
| --- | --- | --- |
|
||||
| `ready` | 传输层就绪,通常携带 `request_message_id`、`response_message_id`、`model_type` | 记录元数据即可,不参与正文拼接 |
|
||||
| `update_session` | 会话时间戳或心跳更新 | 当作会话状态帧处理 |
|
||||
| `message` | 主体载荷,绝大多数业务信息都在这里 | 必须按顺序解析并保序累积 |
|
||||
| `finish` | 生成阶段结束 | 作为流结束标记之一 |
|
||||
| `title` | 会话标题生成结果 | 元数据帧,不参与正文拼接 |
|
||||
| `close` | 连接关闭信息 | 仅用于收尾与审计 |
|
||||
| `ready` | Transport layer ready, usually carrying `request_message_id`, `response_message_id`, `model_type` | Just record the metadata; do not include in body assembly |
|
||||
| `update_session` | Session timestamp or heartbeat update | Treat as a session-state frame |
|
||||
| `message` | The main payload; the vast majority of business information is here | Must be parsed in order and accumulated in order |
|
||||
| `finish` | Generation phase ends | One of the stream-end markers |
|
||||
| `title` | Session title generation result | Metadata frame, not part of body assembly |
|
||||
| `close` | Connection close info | Only for finalization and audit |
|
||||
|
||||
说明:
|
||||
Notes:
|
||||
|
||||
- `message` 是默认事件名,SSE 中没有显式 `event:` 时也应按 `message` 处理。
|
||||
- 目前样本里大量 `message` 帧没有独立的业务前缀,不能靠事件名区分正文和控制帧。
|
||||
- 可能出现空 payload 的 `message` 帧;它们应被视为 no-op,但不能打乱事件顺序。
|
||||
- `message` is the default event name; even when SSE has no explicit `event:`, it should be handled as `message`.
|
||||
- Currently many `message` frames in the samples have no separate business prefix, so you cannot distinguish body vs control frames by event name.
|
||||
- `message` frames with empty payload may appear; they should be treated as no-ops but must not disrupt event order.
|
||||
|
||||
## 4. 载荷层形态
|
||||
## 4. Payload shapes
|
||||
|
||||
`message` 的 `data:` 部分不是单一 schema,而是多种结构混合。当前 corpus 里主要见到以下几种形态:
|
||||
The `data:` part of a `message` is not a single schema, but a mix of structures. The current corpus mainly shows these shapes:
|
||||
|
||||
| 形态 | 典型结构 | 作用 |
|
||||
| Shape | Typical structure | Purpose |
|
||||
| --- | --- | --- |
|
||||
| 初始化 envelope | `{"v":{"response":{...}}}` | 给出会话初始状态、模型状态和片段容器 |
|
||||
| 纯文本 token | `{"v":"..."}` | 直接输出可见文本 token |
|
||||
| 路径补丁 | `{"p":"...","o":"APPEND|SET|BATCH","v":...}` | 对某个状态路径做增量更新 |
|
||||
| 终态 batch | `{"v":[{"p":"status","v":"CONTENT_FILTER"}, ...]}` | 尾部状态收口,常见于风控终态 |
|
||||
| Initialization envelope | `{"v":{"response":{...}}}` | Gives the session's initial state, model state, and fragment container |
|
||||
| Plain text token | `{"v":"..."}` | Directly outputs a visible text token |
|
||||
| Path patch | `{"p":"...","o":"APPEND|SET|BATCH","v":...}` | Applies an incremental update to a state path |
|
||||
| Terminal batch | `{"v":[{"p":"status","v":"CONTENT_FILTER"}, ...]}` | Tail-end state finalization, common in risk-control terminal states |
|
||||
|
||||
一个简化后的典型样式如下:
|
||||
A simplified typical pattern looks like:
|
||||
|
||||
```json
|
||||
{"v":"输出"}
|
||||
{"v":"output"}
|
||||
{"p":"response/fragments/-1/content","o":"APPEND","v":"..."}
|
||||
{"p":"response/fragments","o":"APPEND","v":[...]}
|
||||
{"p":"response","o":"BATCH","v":[{"p":"accumulated_token_usage","v":211},{"p":"quasi_status","v":"FINISHED"}]}
|
||||
{"p":"response/status","o":"SET","v":"FINISHED"}
|
||||
```
|
||||
|
||||
注意:
|
||||
Notes:
|
||||
|
||||
- `v` 可能是字符串、对象、数组、布尔值或数字。
|
||||
- `o` 当前样本里主要见到 `APPEND`、`SET`、`BATCH`。
|
||||
- `v` 为数组时,通常表示一个批量 patch 集合,而不是正文数组。
|
||||
- `v` may be a string, object, array, boolean, or number.
|
||||
- `o` in the current samples is mainly `APPEND`, `SET`, `BATCH`.
|
||||
- When `v` is an array, it usually represents a batch patch set, not a body array.
|
||||
|
||||
## 5. 初始化 envelope
|
||||
## 5. Initialization envelope
|
||||
|
||||
每条流开头,常会先出现一个 `message` 帧,内容是完整的 `response` 初始状态。当前 corpus 中,这个 envelope 常见字段包括:
|
||||
At the start of each stream, a `message` frame often appears first, whose content is the complete initial `response` state. In the current corpus, common fields of this envelope include:
|
||||
|
||||
- `message_id`
|
||||
- `parent_id`
|
||||
@@ -114,29 +114,29 @@ close
|
||||
- `auto_continue`
|
||||
- `search_triggered`
|
||||
|
||||
这些字段更像会话状态和策略开关,不是正文内容。第三方实现应把它们保留在内部状态树里,而不是直接拼接到最终答案。
|
||||
These fields are more like session state and policy switches, not body content. A third-party implementation should keep them in an internal state tree rather than concatenating them directly into the final answer.
|
||||
|
||||
## 6. 路径结构
|
||||
## 6. Path structure
|
||||
|
||||
当前 corpus 里观察到的 `p` 路径可以归成几组:
|
||||
The `p` paths observed in the current corpus can be grouped into a few categories:
|
||||
|
||||
### 6.1 片段级路径
|
||||
### 6.1 Fragment-level paths
|
||||
|
||||
- `response/fragments/-N/content`
|
||||
- `response/fragments/-N/status`
|
||||
- `response/fragments/-N/results`
|
||||
- `response/fragments/-N/elapsed_secs`
|
||||
|
||||
这类路径表示某个片段对象的增量更新。`-N` 只是样本中的索引风格,不应被写死成固定数量。
|
||||
These paths represent incremental updates to a fragment object. `-N` is just the index style in the samples and should not be hardcoded to a fixed count.
|
||||
|
||||
### 6.2 片段容器路径
|
||||
### 6.2 Fragment container paths
|
||||
|
||||
- `response/fragments`
|
||||
- `fragments`
|
||||
|
||||
这两类路径通常承载 fragment 数组。前者更像响应树中的分支,后者更像终态批处理里的片段集合。
|
||||
These two kinds of paths usually carry fragment arrays. The former is more like a branch in the response tree; the latter is more like a fragment set in a terminal batch.
|
||||
|
||||
### 6.3 语义状态路径
|
||||
### 6.3 Semantic state paths
|
||||
|
||||
- `response/status`
|
||||
- `response/has_pending_fragment`
|
||||
@@ -144,38 +144,38 @@ close
|
||||
- `status`
|
||||
- `ban_regenerate`
|
||||
|
||||
这类路径决定流是否结束、是否被风控、是否还有待处理片段。它们不应作为正文输出。
|
||||
These paths determine whether the stream has ended, whether it was filtered by risk control, and whether there are still pending fragments. They should not be output as body.
|
||||
|
||||
尤其是 `response/status` / `status` 这类路径上的字符串值,应被视为控制信号而不是文本 token。当前已确认需要特殊对待的值包括:
|
||||
In particular, string values on paths like `response/status` / `status` should be treated as control signals, not text tokens. Values currently confirmed to need special handling include:
|
||||
|
||||
- `FINISHED`:正常完成终态,应触发收口。
|
||||
- `CONTENT_FILTER`:风控终态,应走拒答/模板分支。
|
||||
- `WIP` / `INCOMPLETE` / `AUTO_CONTINUE`:未完成但可继续生成的中间状态,不应直接输出给客户端。
|
||||
- `FINISHED`: the normal completion terminal state, should trigger finalization.
|
||||
- `CONTENT_FILTER`: the risk-control terminal state, should go to the refusal/template branch.
|
||||
- `WIP` / `INCOMPLETE` / `AUTO_CONTINUE`: incomplete but continuable intermediate states, should not be output directly to the client.
|
||||
|
||||
### 6.4 统计与进度路径
|
||||
### 6.4 Stats and progress paths
|
||||
|
||||
- `accumulated_token_usage`
|
||||
|
||||
这类路径用于使用量或进度统计,属于元数据。
|
||||
These paths are for usage or progress stats and are metadata.
|
||||
|
||||
### 6.5 非命名空间字段
|
||||
### 6.5 Non-namespaced fields
|
||||
|
||||
在片段对象内部,还会看到 `content`、`references`、`result`、`queries`、`stage_id` 等字段。它们不一定带 `response/...` 前缀,但仍然是协议语义的一部分。
|
||||
Inside a fragment object, you will also see fields like `content`, `references`, `result`, `queries`, `stage_id`. They do not necessarily carry the `response/...` prefix, but are still part of the protocol semantics.
|
||||
|
||||
## 7. fragment 类型
|
||||
## 7. Fragment types
|
||||
|
||||
当前 corpus 里已经观察到的 fragment 类型如下:
|
||||
The fragment types observed in the current corpus are:
|
||||
|
||||
| 类型 | 作用 | 是否应直接渲染 |
|
||||
| Type | Purpose | Render directly? |
|
||||
| --- | --- | --- |
|
||||
| `RESPONSE` | 正常回答片段 | 是,属于正文 |
|
||||
| `THINK` | 推理或阶段提示 | 通常否,按产品策略决定是否展示 |
|
||||
| `TOOL_SEARCH` | 搜索工具调用元数据 | 否 |
|
||||
| `TOOL_OPEN` | 打开 / 抽取结果的工具元数据 | 否 |
|
||||
| `TIP` | 提示 / 警告类片段,常带 `style: WARNING` | 视产品策略决定,通常作为附注 |
|
||||
| `TEMPLATE_RESPONSE` | 风控拒答模板 | 是,但它属于终态 fallback,不是普通正文 |
|
||||
| `RESPONSE` | Normal answer fragment | Yes, it is body |
|
||||
| `THINK` | Reasoning or stage hint | Usually no; depends on product policy |
|
||||
| `TOOL_SEARCH` | Search tool-call metadata | No |
|
||||
| `TOOL_OPEN` | Open / extract-result tool metadata | No |
|
||||
| `TIP` | Hint / warning fragment, often with `style: WARNING` | Depends on product policy, usually a footnote |
|
||||
| `TEMPLATE_RESPONSE` | Risk-control refusal template | Yes, but it is a terminal fallback, not ordinary body |
|
||||
|
||||
观察到的典型片段字段:
|
||||
Typical fragment fields observed:
|
||||
|
||||
- `id`
|
||||
- `type`
|
||||
@@ -190,39 +190,39 @@ close
|
||||
- `style`
|
||||
- `hide_on_wip`
|
||||
|
||||
第三方实现不要把 `fragment.type` 和 `p` 路径混为一谈。`type` 是语义分类,`p` 是状态树位置。
|
||||
Third-party implementations should not conflate `fragment.type` with the `p` path. `type` is a semantic category; `p` is a position in the state tree.
|
||||
|
||||
## 8. 终态行为
|
||||
## 8. Terminal behavior
|
||||
|
||||
当前 corpus 里有两条很重要的终态分支。
|
||||
There are two very important terminal branches in the current corpus.
|
||||
|
||||
### 8.1 正常完成
|
||||
### 8.1 Normal completion
|
||||
|
||||
正常回答通常会出现如下收口顺序:
|
||||
A normal answer usually shows the following finalization sequence:
|
||||
|
||||
1. `response` 的 `BATCH` 更新 `accumulated_token_usage`。
|
||||
2. `response` 的 `BATCH` 或单独 patch 更新 `quasi_status: FINISHED`。
|
||||
3. `response/status` 置为 `FINISHED`。
|
||||
4. `finish` 事件到来。
|
||||
5. 之后可能还有 `update_session`、`title`、`close`。
|
||||
1. A `BATCH` update of `response` sets `accumulated_token_usage`.
|
||||
2. A `BATCH` of `response`, or a standalone patch, sets `quasi_status: FINISHED`.
|
||||
3. `response/status` is set to `FINISHED`.
|
||||
4. The `finish` event arrives.
|
||||
5. Afterwards there may still be `update_session`, `title`, `close`.
|
||||
|
||||
### 8.2 风控终态
|
||||
### 8.2 Risk-control terminal state
|
||||
|
||||
`content-filter-trigger-20260405-jwt3` 展示了另一种终态路径:
|
||||
`content-filter-trigger-20260405-jwt3` shows another terminal path:
|
||||
|
||||
1. 先继续输出一段正常正文。
|
||||
2. 出现提示类 fragment,例如 `TIP`。
|
||||
3. 可能先把 `quasi_status` 提前收口到 `FINISHED`。
|
||||
4. 之后出现一个终态 batch,把 `ban_regenerate` 设为 `true`,把 `status` 置为 `CONTENT_FILTER`,并附带 `TEMPLATE_RESPONSE`。
|
||||
5. 最后再出现 `finish`,然后是收尾事件。
|
||||
1. First continue outputting some normal body.
|
||||
2. A hint-type fragment appears, e.g. `TIP`.
|
||||
3. It may finalize `quasi_status` to `FINISHED` early.
|
||||
4. Then a terminal batch appears, setting `ban_regenerate` to `true`, setting `status` to `CONTENT_FILTER`, and including a `TEMPLATE_RESPONSE`.
|
||||
5. Finally `finish` appears, followed by the finalization events.
|
||||
|
||||
这个分支说明:
|
||||
This branch shows that:
|
||||
|
||||
- `finish` 不等于正常结束。
|
||||
- `CONTENT_FILTER` 是一个独立终态,不是普通异常。
|
||||
- `TEMPLATE_RESPONSE` 不应被当作常规回答流的中间片段,它是终态 fallback。
|
||||
- `finish` does not equal normal completion.
|
||||
- `CONTENT_FILTER` is an independent terminal state, not an ordinary error.
|
||||
- `TEMPLATE_RESPONSE` should not be treated as an intermediate fragment of the normal answer stream; it is a terminal fallback.
|
||||
|
||||
一个简化的风控尾部可以写成:
|
||||
A simplified risk-control tail can be written as:
|
||||
|
||||
```json
|
||||
{"p":"response","o":"BATCH","v":[{"p":"accumulated_token_usage","v":1269},{"p":"quasi_status","v":"FINISHED"}]}
|
||||
@@ -230,86 +230,86 @@ close
|
||||
{"event":"finish"}
|
||||
```
|
||||
|
||||
### 8.3 自动续写中间态(实现补充)
|
||||
### 8.3 Automatic continuation intermediate state (implementation addendum)
|
||||
|
||||
这部分不是当前 corpus 的直接覆盖项,而是 2026-04-05 在长思考实测中观察到、且已在当前实现中兼容的行为:
|
||||
This part is not directly covered by the current corpus, but is behavior observed during long-thinking testing on 2026-04-05 and already supported in the current implementation:
|
||||
|
||||
1. 上游可能先把 `response/status` 或 envelope 内的 `response.status` 置为 `WIP` / `INCOMPLETE`。
|
||||
2. 有时还会伴随 `auto_continue: true`。
|
||||
3. 这表示当前轮输出尚未真正结束,客户端或代理层可以继续调用 continue 接口续写同一条回答。
|
||||
4. 续写后的内容会承接之前的思考与正文,不应把前一轮状态值泄露成可见文本。
|
||||
1. The upstream may first set `response/status`, or `response.status` inside the envelope, to `WIP` / `INCOMPLETE`.
|
||||
2. Sometimes it is also accompanied by `auto_continue: true`.
|
||||
3. This means the current turn's output has not truly ended; the client or proxy layer can keep calling the continue endpoint to extend the same answer.
|
||||
4. The continued content carries on from the previous thinking and body, and must not leak the previous turn's state values as visible text.
|
||||
|
||||
对第三方实现,建议把这一类状态统一当作“可继续的控制信号”:
|
||||
For third-party implementations, it is recommended to treat this kind of state uniformly as a "continuable control signal":
|
||||
|
||||
- 可以据此决定是否继续拉取后续流。
|
||||
- 不能把 `INCOMPLETE`、`WIP`、`AUTO_CONTINUE` 直接拼接到最终文本。
|
||||
- `finish` 事件本身也不能单独说明回答已完全结束,仍要结合状态字段判断。
|
||||
- Use it to decide whether to keep pulling the subsequent stream.
|
||||
- Do not concatenate `INCOMPLETE`, `WIP`, `AUTO_CONTINUE` directly into the final text.
|
||||
- The `finish` event itself cannot solely indicate the answer is fully complete; you still have to judge by the state fields.
|
||||
|
||||
## 9. 文本重建规则
|
||||
## 9. Text reconstruction rules
|
||||
|
||||
如果你的目标是把流重建成最终可见文本,必须遵守下面这些规则:
|
||||
If your goal is to reconstruct the stream into the final visible text, you must follow these rules:
|
||||
|
||||
- 按接收顺序逐个追加 token。
|
||||
- 不要对每个 `v` 做 `trim` 或 `TrimSpace`。
|
||||
- 不要丢弃只包含空格的 chunk。
|
||||
- 不要合并连续空格、换行或 Markdown 符号附近的空白。
|
||||
- 不要把 `[reference:N]` 视为协议元数据,它在当前 corpus 里就是正文的一部分。
|
||||
- 如果你要屏蔽引用标记,应当把它做成可配置的后处理,而不是在解析阶段硬删。
|
||||
- `response/status` / `status` 路径上的状态字符串不应进入正文,即使它们不是终态。
|
||||
- Append tokens one by one in receive order.
|
||||
- Do not `trim` or `TrimSpace` each `v`.
|
||||
- Do not drop chunks that contain only whitespace.
|
||||
- Do not merge consecutive spaces, newlines, or whitespace near Markdown symbols.
|
||||
- Do not treat `[reference:N]` as protocol metadata; in the current corpus it is part of the body.
|
||||
- If you want to hide citation markers, make it a configurable post-processing step rather than hard-deleting it at the parsing stage.
|
||||
- State strings on `response/status` / `status` paths should not enter the body, even when they are not terminal.
|
||||
|
||||
这点对 Markdown、代码块、引用、表格都很关键。样本里已经证明,`#`、`-`、`>`、`|` 这类符号后面的空格必须原样保留,否则渲染结果会变形。
|
||||
This is crucial for Markdown, code blocks, quotes, and tables. The samples have already proven that the whitespace after symbols like `#`, `-`, `>`, `|` must be preserved as-is, otherwise the rendered result is distorted.
|
||||
|
||||
## 10. 推荐实现方式
|
||||
## 10. Recommended implementation
|
||||
|
||||
对第三方开发者,建议把实现拆成三条线:
|
||||
For third-party developers, it is recommended to split the implementation into three tracks:
|
||||
|
||||
1. 原始事件线:保留 SSE block 顺序、事件名和完整 JSON 载荷。
|
||||
2. 状态树线:维护 `response`、`fragments`、`status`、`quasi_status` 等结构。
|
||||
3. 可见文本线:只从明确应渲染的 token / fragment 中拼接最终文本。
|
||||
1. Raw event track: preserve SSE block order, event names, and the full JSON payloads.
|
||||
2. State tree track: maintain structures like `response`, `fragments`, `status`, `quasi_status`.
|
||||
3. Visible text track: assemble the final text only from tokens / fragments that are clearly meant to be rendered.
|
||||
|
||||
一个简单的处理顺序可以是:
|
||||
A simple processing order can be:
|
||||
|
||||
```text
|
||||
parse SSE block
|
||||
-> 识别 event
|
||||
-> 解析 JSON payload
|
||||
-> 更新状态树
|
||||
-> 识别 status / quasi_status / auto_continue 等控制信号
|
||||
-> 判定是否有可见文本
|
||||
-> 追加到输出缓冲
|
||||
-> 遇到 WIP / INCOMPLETE / AUTO_CONTINUE 时决定是否续写
|
||||
-> 遇到 FINISHED / CONTENT_FILTER / finish 时收口
|
||||
-> identify event
|
||||
-> parse JSON payload
|
||||
-> update state tree
|
||||
-> identify control signals like status / quasi_status / auto_continue
|
||||
-> determine whether there is visible text
|
||||
-> append to the output buffer
|
||||
-> on WIP / INCOMPLETE / AUTO_CONTINUE, decide whether to continue
|
||||
-> on FINISHED / CONTENT_FILTER / finish, finalize
|
||||
```
|
||||
|
||||
实现时的兼容原则:
|
||||
Compatibility principles when implementing:
|
||||
|
||||
- 未知路径保留,不要报错中断。
|
||||
- 未知 fragment.type 保留在日志里。
|
||||
- 不要假设所有模型都一定输出 `thinking_content`,当前 corpus 的推理更多是通过 fragment 类型表达。
|
||||
- 不要假设 `title` 一定存在,它只是后置元数据。
|
||||
- Preserve unknown paths; do not error out and abort.
|
||||
- Keep unknown fragment.type values in the logs.
|
||||
- Do not assume every model necessarily outputs `thinking_content`; reasoning in the current corpus is mostly expressed through fragment types.
|
||||
- Do not assume `title` always exists; it is just post-hoc metadata.
|
||||
|
||||
## 11. 本 corpus 证明了什么
|
||||
## 11. What this corpus proves
|
||||
|
||||
当前样本足以证明以下行为:
|
||||
The current samples are sufficient to prove the following behaviors:
|
||||
|
||||
- 搜索类模型会把工具调用、结果、引用和正文混在同一条 SSE 流里。
|
||||
- 风控不会简单地“没有输出”,而是会在正常生成后切换到 `CONTENT_FILTER` 终态。
|
||||
- Markdown 和代码输出对空格非常敏感,空格 chunk 不能吞。
|
||||
- `message` 是主体承载层,`ready` / `update_session` / `finish` / `title` / `close` 是控制层。
|
||||
- `fragment.type` 是可视化和工具链分层的关键,不应只靠 `p` 路径判断。
|
||||
- Search-type models mix tool calls, results, citations, and body into the same SSE stream.
|
||||
- Risk control is not simply "no output"; it switches to the `CONTENT_FILTER` terminal state after normal generation.
|
||||
- Markdown and code output are very whitespace-sensitive; whitespace chunks must not be swallowed.
|
||||
- `message` is the main carrier layer; `ready` / `update_session` / `finish` / `title` / `close` are the control layer.
|
||||
- `fragment.type` is key to visualization and tool-chain layering and should not be judged by the `p` path alone.
|
||||
|
||||
结合 2026-04-05 的长思考实测,还可以补充一条当前实现层面的结论:
|
||||
Combined with the long-thinking testing on 2026-04-05, one more implementation-level conclusion can be added:
|
||||
|
||||
- 长思考场景下,上游可能先给出 `INCOMPLETE` / `WIP` / `AUTO_CONTINUE` 状态,再通过 continue 链路续写;这些状态值本身不应作为正文输出。
|
||||
- In long-thinking scenarios, the upstream may first give an `INCOMPLETE` / `WIP` / `AUTO_CONTINUE` state, then continue via the continue path; these state values themselves should not be output as body.
|
||||
|
||||
## 12. 适用边界
|
||||
## 12. Scope of applicability
|
||||
|
||||
本文是基于当前 corpus 的逆向说明,不是恒定协议。
|
||||
This document is a reverse-engineering description based on the current corpus, not a permanent protocol.
|
||||
|
||||
- 新模型可能增加新的 `p` 路径。
|
||||
- 新版本可能增加新的 fragment.type。
|
||||
- `CONTENT_FILTER` 的终态模板内容可能变化。
|
||||
- 自动续写相关状态(如 `INCOMPLETE` / `AUTO_CONTINUE`)当前主要来自实测与实现兼容逻辑,后续字段形态仍可能变化。
|
||||
- 解析器应当对未知字段、未知路径、未知事件保持容忍。
|
||||
- New models may add new `p` paths.
|
||||
- New versions may add new fragment.type values.
|
||||
- The terminal template content of `CONTENT_FILTER` may change.
|
||||
- Automatic-continuation states (such as `INCOMPLETE` / `AUTO_CONTINUE`) currently come mostly from live testing and implementation compatibility logic; the field shapes may still change later.
|
||||
- The parser should remain tolerant of unknown fields, unknown paths, and unknown events.
|
||||
|
||||
如果你要把这份说明用于实际开发,建议同时保留原始流样本、回放脚本和回归测试,不要只依赖本文。
|
||||
If you want to use this description for actual development, it is recommended to also keep the raw stream samples, replay scripts, and regression tests, and not rely on this document alone.
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
# 原始流数据样本目录
|
||||
# Raw stream sample directory
|
||||
|
||||
该目录只保留**上游真实 SSE 原始流**,用于本地回放、字段分析和回归测试。
|
||||
This directory keeps only **real upstream SSE raw streams**, used for local replay, field analysis, and regression testing.
|
||||
|
||||
## 样本分类
|
||||
## Sample categories
|
||||
|
||||
该目录下的样本分成两类:
|
||||
The samples in this directory fall into two categories:
|
||||
|
||||
- canonical 默认样本:由 [`manifest.json`](./manifest.json) 的 `default_samples` 指定,默认回放工具优先跑这组稳定样本
|
||||
- 扩展样本:保留真实问题或特定协议行为,用于排障、字段分析和定向回归,不一定默认纳入全量回放
|
||||
- canonical default samples: specified by `default_samples` in [`manifest.json`](./manifest.json); the default replay tool runs this stable set first
|
||||
- extended samples: preserve real issues or specific protocol behaviors for troubleshooting, field analysis, and targeted regression; not necessarily included in the full replay by default
|
||||
|
||||
当前目录里除了 canonical 样本,还包含例如:
|
||||
Besides the canonical samples, the directory currently also contains, for example:
|
||||
|
||||
- `markdown-format-example-20260405`
|
||||
- `markdown-format-example-20260405-spacefix`
|
||||
- `continue-thinking-snapshot-replay-20260405`
|
||||
|
||||
其中 `continue-thinking-snapshot-replay-20260405` 是一个多轮样本,覆盖 `completion + continue` 的原始 SSE 重放场景,用于验证接续思考去重。
|
||||
Among these, `continue-thinking-snapshot-replay-20260405` is a multi-turn sample covering the `completion + continue` raw SSE replay scenario, used to verify continued-thinking deduplication.
|
||||
|
||||
如果要看默认固定回放集,以 [`manifest.json`](./manifest.json) 为准,而不是按目录数量人工判断。
|
||||
更完整的协议级行为结构说明见 [docs/DeepSeekSSE行为结构说明-2026-04-05.md](../../docs/DeepSeekSSE行为结构说明-2026-04-05.md)。
|
||||
To see the default fixed replay set, rely on [`manifest.json`](./manifest.json) rather than judging by the number of directories.
|
||||
For a more complete protocol-level behavior description, see [docs/deepseek-sse-behavior-2026-04-05.md](../../docs/deepseek-sse-behavior-2026-04-05.md).
|
||||
|
||||
## 自动采集接口
|
||||
## Automatic capture endpoint
|
||||
|
||||
本地启动服务后,可以直接调用专用接口自动落盘一份 raw-only 样本:
|
||||
After starting the service locally, you can call the dedicated endpoint to automatically persist a raw-only sample:
|
||||
|
||||
```bash
|
||||
POST /admin/dev/raw-samples/capture
|
||||
```
|
||||
|
||||
这个接口会:
|
||||
This endpoint will:
|
||||
|
||||
- 接收一个普通的 OpenAI chat completions 请求体
|
||||
- 走项目内同一条处理链
|
||||
- 自动保存请求元信息 `meta.json`
|
||||
- 自动保存上游原始流 `upstream.stream.sse`
|
||||
- Accept an ordinary OpenAI chat completions request body
|
||||
- Run it through the same processing chain in the project
|
||||
- Automatically save the request metadata `meta.json`
|
||||
- Automatically save the upstream raw stream `upstream.stream.sse`
|
||||
|
||||
采集接口的响应体仍然是项目当次的实际输出,但它不会再写入样本目录。这样样本树始终只保留原始流,后续回放时再按需本地生成派生结果。
|
||||
The capture endpoint's response body is still the project's actual output for that call, but it is no longer written into the sample directory. This way the sample tree always keeps only the raw stream, and derived results are generated locally on demand during later replay.
|
||||
|
||||
如果问题已经在当前进程的内存抓包里复现过,也可以先查再存:
|
||||
If the issue has already been reproduced in the current process's in-memory captures, you can query first and then save:
|
||||
|
||||
```bash
|
||||
GET /admin/dev/raw-samples/query?q=关键词&limit=20
|
||||
GET /admin/dev/raw-samples/query?q=keyword&limit=20
|
||||
POST /admin/dev/raw-samples/save
|
||||
```
|
||||
|
||||
这条链路适合把“刚刚发生的一次真实问题”快速转成可回放样本,而不用重新触发请求。
|
||||
This path is suitable for quickly turning "a real issue that just happened" into a replayable sample without re-triggering the request.
|
||||
|
||||
## 目录规范
|
||||
## Directory conventions
|
||||
|
||||
每个样本一个子目录,且只保留下面两类文件:
|
||||
One subdirectory per sample, keeping only the following two kinds of files:
|
||||
|
||||
- `meta.json`:样本元信息(问题、模型、采集时间、备注)
|
||||
- `upstream.stream.sse`:完整原始 SSE 文本(`event:` / `data:` 行)
|
||||
- `meta.json`: sample metadata (question, model, capture time, notes)
|
||||
- `upstream.stream.sse`: the full raw SSE text (`event:` / `data:` lines)
|
||||
|
||||
`meta.json` 的关键字段通常包括:
|
||||
Key fields of `meta.json` usually include:
|
||||
|
||||
- `sample_id`
|
||||
- `captured_at_utc`
|
||||
@@ -61,22 +61,22 @@ POST /admin/dev/raw-samples/save
|
||||
- `request`
|
||||
- `capture`
|
||||
|
||||
对于多轮样本,`capture.rounds` 会记录每一轮上游请求,例如首轮 `deepseek_completion` 和后续 `deepseek_continue`。
|
||||
For multi-turn samples, `capture.rounds` records each upstream request round, e.g. the first round `deepseek_completion` and the subsequent `deepseek_continue`.
|
||||
|
||||
## 回放与对比
|
||||
## Replay and comparison
|
||||
|
||||
回放工具会读取 `upstream.stream.sse`,在本地自动生成当前解析结果,并把派生结果写到 `artifacts/raw-stream-sim/<run-id>/<sample-id>/`,例如:
|
||||
The replay tool reads `upstream.stream.sse`, generates the current parsing result locally, and writes the derived results to `artifacts/raw-stream-sim/<run-id>/<sample-id>/`, for example:
|
||||
|
||||
- `replay.output.txt`:本次回放生成的最终可见文本
|
||||
- `report.json`:本次回放的结构化报告,包含事件数、chunk 数、终态、引用泄露检查等信息
|
||||
- `replay.output.txt`: the final visible text generated by this replay
|
||||
- `report.json`: the structured report of this replay, including event count, chunk count, terminal state, citation-leak checks, etc.
|
||||
|
||||
运行全部 canonical 样本:
|
||||
Run all canonical samples:
|
||||
|
||||
```bash
|
||||
./tests/scripts/run-raw-stream-sim.sh
|
||||
```
|
||||
|
||||
运行**全部样本目录**(不只 manifest 默认样本),并逐个打印 token 对齐结果:
|
||||
Run **all sample directories** (not just the manifest default samples), printing the token alignment result for each:
|
||||
|
||||
```bash
|
||||
for d in tests/raw_stream_samples/*; do
|
||||
@@ -87,24 +87,24 @@ for d in tests/raw_stream_samples/*; do
|
||||
done
|
||||
```
|
||||
|
||||
回放输出会显示 `tokens=<parsed>/<expected>`;默认只记录 token 差异,不因 token 不一致失败。如需把 token 差异作为失败条件,给模拟器增加 `--fail-on-token-mismatch`。`report.json` 中也会包含:
|
||||
The replay output shows `tokens=<parsed>/<expected>`; by default it only records token differences and does not fail on token mismatch. To make a token difference a failure condition, add `--fail-on-token-mismatch` to the simulator. `report.json` also includes:
|
||||
|
||||
- `raw_expected_output_tokens`
|
||||
- `raw_parsed_output_tokens`
|
||||
- `raw_token_mismatch`
|
||||
|
||||
运行单个样本并和已有基线比对:
|
||||
Run a single sample and compare against an existing baseline:
|
||||
|
||||
```bash
|
||||
./tests/scripts/compare-raw-stream-sample.sh markdown-format-example-20260405-spacefix
|
||||
```
|
||||
|
||||
如果你已经有历史基线目录,也可以把它作为第二个参数传进去,脚本会对比当前回放结果和基线输出。
|
||||
If you already have a historical baseline directory, you can pass it as the second argument, and the script will compare the current replay result against the baseline output.
|
||||
|
||||
## 扩展方式
|
||||
## How to extend
|
||||
|
||||
1. 抓取一次真实请求。
|
||||
2. 直接调用 `/admin/dev/raw-samples/capture`,或者先用 `/admin/dev/raw-samples/query` + `/admin/dev/raw-samples/save` 从内存抓包落盘;也可以手工新建 `<sample-id>/` 目录并放入 `meta.json` + `upstream.stream.sse`。
|
||||
3. 运行回放工具或对比脚本,生成本地派生结果并检查是否回归。
|
||||
1. Capture a real request.
|
||||
2. Either call `/admin/dev/raw-samples/capture` directly, or first persist from in-memory captures via `/admin/dev/raw-samples/query` + `/admin/dev/raw-samples/save`; you can also manually create a `<sample-id>/` directory and put `meta.json` + `upstream.stream.sse` in it.
|
||||
3. Run the replay tool or comparison script to generate local derived results and check for regressions.
|
||||
|
||||
> 注意:样本可能包含搜索结果正文与引用信息,请勿放入敏感账号/密钥。
|
||||
> Note: samples may contain search-result body text and citation info, so do not include sensitive accounts/keys.
|
||||
|
||||
Reference in New Issue
Block a user