DS2API citation/reference零基vs一基序号解析细节全解
【免费下载链接】ds2apiDeepSeek-Compatible Middleware Interface: A technical exploration project in Go, focusing on high-concurrency protocol adaptation. It serves as a reference implementation for converting diverse web protocols into standardized formats.项目地址: https://gitcode.com/GitHub_Trending/ds/ds2api
DS2API 是一款基于 Go 的DeepSeek 兼容中间件,负责把上游 SSE 流转换成 OpenAI / Claude / Gemini 等标准协议。开启联网搜索后,回答里会出现[citation:1]、[reference:0]这类引用序号标记——但上游的序号到底是零基(0 开始)还是一基(1 开始)?DS2API 用"双流收集 + 冲突仲裁"的策略统一处理了这个问题。本文带你从新手视角看懂这条引用解析链路的每个细节。
📌 一分钟看懂:引用标记的"序号陷阱"
DeepSeek 上游流里,引用信息分散在两处:
| 来源 | 示例 | 序号体系 |
|---|---|---|
| 回答正文中的标记 | [citation:1]、[reference:0] | 可能是零基,也可能是一基 |
SSE 元数据(url+cite_index) | 片段附带的来源链接 | 同样存在零基/一基两种版本 |
麻烦在于:正文标记的起点和元数据cite_index的起点未必一致。DS2API 的解法分两步——先在流中"收集",最后再"替换",每一步都对零基/一基做了防御。
🔍 第一步:SSE 流中的引用收集
核心实现在 internal/sse/citation_links.go,收集器维护两条轨道:
ordered:按出现顺序记录每条http(s)来源 URL(隐含一基序号,第 i 条就是序号 i);explicitRaw:记录元数据里显式的cite_index → url映射。
真正的零基/一基判定发生在 buildNormalizedExplicit():
- 默认按一基处理:正数索引原样保留,零索引被跳过;
- 一旦出现过
cite_index = 0(即 hasZeroIdx 被置位),说明上游大概率使用零基体系,于是给每个索引追加一个"+1 偏移候选"; - 冲突仲裁:同一目标序号同时存在"原始值"和"+1 偏移值"时,由 preferURLForIndex() 裁决——谁的 URL 与
ordered中该序号位置的实际出现顺序吻合,就选谁。
💡 为什么用"出现顺序"当裁判?因为来源 URL 在流里的到达顺序天然等于它在正文中的引用顺序,是最可靠的锚点。
最后由 build() 输出:显式索引优先,空缺位置用ordered按一基(idx = i + 1)补位。最终产物是一张一基的map[序号]URL。
✍️ 第二步:把序号标记替换成 Markdown 链接
替换逻辑在 internal/httpapi/openai/shared/citation_links.go:
- 用正则(忽略大小写、允许冒号后空格)匹配
[citation:N]和[reference:N]两种标记; - 查表时默认
lookupIdx = N(一基直查); - 关键细节:如果正文里出现了
[reference:0](说明 reference 标记是零基的),则仅对reference标记做N + 1偏移查表,而[citation:N]仍按一基处理——两种标记各自独立判定基线; - 替换后展示数字保留原文:零基的
[reference:0]会渲染成0,不强行改成 1,避免与正文语义错位; - 查不到的序号原样保留,绝不瞎配链接。
这些行为都有对应测试用例兜底,见 citation_links_test.go,其中"零基 reference 与一基 citation 混用"的用例最能说明设计意图。
🌊 流式输出时:为什么标记会被暂时隐藏?
流式场景有个尴尬点:正文先到、引用元数据可能晚于FINISHED状态到达(consumer.go 特意在结束后继续扫描元数据直到[DONE])。因此在链接表就绪前,DS2API 会先剥掉未定型的标记:
- Go 端:StripReferenceMarkers(),流式面默认开启;
- 浏览器端 JS 代理做了同样的事:stripReferenceMarkersText() 用相同正则清洗。
流结束后统一走第二步替换,用户最终看到的是干净的可点击链接。
✅ 新手自查清单
cite_index从 0 开始 → 上游零基,靠hasZeroIdx触发 +1 候选;- 只有正数索引 → 按一基直用;
- 两种候选冲突 → 以 URL 出现顺序为仲裁;
[reference:0]只影响 reference 查表偏移,citation 不动;- 查不到链接的序号 → 保留原文,流式中先隐藏。
📂 相关源码与文档
- 收集器(零基/一基判定核心):internal/sse/citation_links.go
- 标记替换(正则 + 偏移查表):internal/httpapi/openai/shared/citation_links.go
- 流式标记剥离:internal/textclean/reference_markers.go
- JS 端同款清洗:internal/js/chat-stream/sse_parse_impl.js
- 单元测试:internal/httpapi/openai/citation_links_test.go
- 架构总览:docs/ARCHITECTURE.md
读懂这套"双轨收集 + 基线仲裁"的设计,你就能理解 DS2API 如何在多种上游变体下,稳定地把引用序号还原成正确的来源链接。
【免费下载链接】ds2apiDeepSeek-Compatible Middleware Interface: A technical exploration project in Go, focusing on high-concurrency protocol adaptation. It serves as a reference implementation for converting diverse web protocols into standardized formats.项目地址: https://gitcode.com/GitHub_Trending/ds/ds2api
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考