DS2API citation/reference零基vs一基序号解析细节全解
2026/9/16 11:14:43 网站建设 项目流程

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():

  1. 默认按一基处理:正数索引原样保留,零索引被跳过;
  2. 一旦出现过cite_index = 0(即 hasZeroIdx 被置位),说明上游大概率使用零基体系,于是给每个索引追加一个"+1 偏移候选";
  3. 冲突仲裁:同一目标序号同时存在"原始值"和"+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() 用相同正则清洗。

流结束后统一走第二步替换,用户最终看到的是干净的可点击链接。

✅ 新手自查清单

  1. cite_index从 0 开始 → 上游零基,靠hasZeroIdx触发 +1 候选;
  2. 只有正数索引 → 按一基直用;
  3. 两种候选冲突 → 以 URL 出现顺序为仲裁;
  4. [reference:0]只影响 reference 查表偏移,citation 不动;
  5. 查不到链接的序号 → 保留原文,流式中先隐藏。

📂 相关源码与文档

  • 收集器(零基/一基判定核心):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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询