Public API 文档

onc.md Public API

Public API 是面向外部系统和 AI Agent 的只读接口,仅返回已发布版本的数据。它提供三类能力:文献查阅、语义检索、带引用的问答(RAG)。

提示:本文档为静态摘要。建议同时访问 Swagger 交互式文档获得准确、完整的接口定义: /api/public/v1/docs(支持在线调试)


Base URL

所有端点前缀:/api/public/v1

鉴权

所有请求必须在 header 中提供 API Key(图片代理端点 /images/{path} 除外):

X-API-Key: <your-api-key>

或者使用 Bearer 格式:

Authorization: Bearer <your-api-key>

API Key 在后台「设置 → API 管理」中创建。创建后请立即复制保存,关闭后将无法再次查看密钥原文。


Endpoints

GET    /guidelines             列出已发布的文献
GET    /guidelines/{id}        获取单篇文献详情 + 完整正文
GET    /guidelines/{id}/body   同上(别名)
POST   /search                 跨库语义检索,返回原文段落
POST   /ask                    问答(RAG),返回带引用角标的答案
POST   /ask/stream             同 /ask,SSE 流式响应(推荐浏览器使用)
GET    /images/{path}          图片代理(无需鉴权)

GET /guidelines

列出所有已发布的文献,支持多维过滤和分页。

Query 参数:

| 参数 | 类型 | 默认值 | 说明 | |---|---|---|---| | publisher | string | - | 过滤发布机构(如 CSCOCMA) | | year | integer | - | 过滤年份 | | tag | string | - | 过滤标签(如 lung) | | skip | integer | 0 | 分页偏移 | | limit | integer | 20 | 每页条数(最大 100) | | sort_by | string | created_at | 排序字段 | | sort_order | string | desc | 排序方向(asc / desc) |

响应结构:

{
  "items": [
    {
      "id": "34c0458349ce4c7bb5ebbaa3e3f70224",
      "title": "中华医学会指南:肺癌临床诊疗指南(2025年版)",
      "publisher": "CMA",
      "publisher_id": null,
      "year": 2025,
      "version": "1.0",
      "tags": ["肺癌", "指南"],
      "is_indexed": true,
      "chunk_count": 124,
      "created_at": "2025-10-20T02:34:48"
    }
  ],
  "pagination": {
    "total": 5,
    "skip": 0,
    "limit": 20,
    "has_more": false
  },
  "sort": {
    "sort_by": "created_at",
    "sort_order": "desc"
  }
}

GET /guidelines/{id}

获取单篇文献的元数据和完整 Markdown 正文。

GET /guidelines/{id}/body 行为完全一致,两者互为别名。

响应字段:

| 字段 | 类型 | 说明 | |---|---|---| | id | string | 文献 ID | | title | string | 标题(含发布机构名称) | | publisher | string | 发布机构 | | year | integer | 年份 | | version | string | 版本号 | | tags | string[] | 标签 | | is_indexed | boolean | 是否已建立索引 | | chunk_count | integer | 索引分块数 | | extracted_text | string | 完整 Markdown 正文(含图片引用、页码标记) | | created_at | datetime | 创建时间 |


POST /search

语义检索端点。 根据关键词跨所有已发布文献检索,返回最匹配的原文段落(完整章节上下文)及引用元数据。

定位:输入是检索关键词,输出是原文。不经过 LLM 改写,适合需要原文溯源的场景。

请求:

{
  "query": "EGFR突变阳性晚期非小细胞肺癌一线治疗",
  "top_k": 5,
  "guideline_id": null
}

| 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | query | string | 是 | 搜索问题(1-2000 字符) | | top_k | integer | 否 | 返回结果数(1-20,默认 5) | | guideline_id | string | 否 | 限定搜索范围到单篇文献 |

响应结构:

{
  "search": {
    "query": "EGFR突变阳性晚期非小细胞肺癌一线治疗",
    "top_k": 5,
    "guideline_id": null,
    "result_count": 5
  },
  "results": [
    {
      "rank": 1,
      "score": 0.862,
      "text": "(原文段落,已去除 embedding 元数据前缀,直接可读)",
      "guideline_id": "9deec8c1ea024486b50bae9f708e8ef3",
      "guideline_title": "中国临床肿瘤学会(CSCO)原发性肺癌诊疗指南(2016.V1)",
      "publisher": "CSCO",
      "year": 2026,
      "version_label": "1.0",
      "section_path": "四、基于病理类型... > 5.IV 期驱动基因阳性非小细胞肺癌的治疗 > 注释:",
      "source_pages": [27, 28, 29, 30, 31],
      "citation_label": "中国临床肿瘤学会(CSCO)... > v1.0 > 四、... > 注释:",
      "evidence_level": "",
      "source_start": 44730,
      "associated_images": [
        {
          "url": "/api/public/v1/images/9deec8c1ea024486b50bae9f708e8ef3/v9/page_28_visual_1.png",
          "type": "image",
          "description": "表1 EGFR-TKI 一线治疗研究汇总",
          "page": 28
        }
      ]
    }
  ],
  "agent_context": {
    "query": "EGFR突变阳性晚期非小细胞肺癌一线治疗",
    "passages": [ /* 同 results,为 AI Agent 设计的精简结构 */ ],
    "suggested_usage": "These are original text passages from published oncology guidelines..."
  }
}

各字段说明:

| 字段 | 说明 | |---|---| | rank | 排名(1-based) | | score | 语义相似度评分(0-1) | | text | 原文段落(完整章节上下文,已清理 embedding 前缀,可直接展示) | | section_path | 完整的章节路径(从顶级标题到最细粒度小节,用 > 分隔) | | citation_label | 可直接展示的引用字符串(标题 > 版本 > 章节路径) | | source_pages | 来源页码列表 | | source_start | 该段落在文献 Markdown 正文中的字符偏移量,用于精确定位原文位置 | | evidence_level | 证据等级/推荐等级(如 1类推荐证据),从文本中自动提取 | | associated_images | 相关图片列表(流程图、表格等),url 为相对路径 |

关于 source_start

返回的是该结果在文献 Markdown 正文中的字符偏移量(0-based)。可用于精确定位原文位置——例如在前端review页面通过 /review/{guideline_id}?sourceOffset={source_start} 跳转到对应位置,避免章节名重复(如多个"注释"小节)导致的定位歧义。

检索机制:

  • 跨所有已发布文献检索,按 section 级别去重(同一文献的同一章节只返回一条最高分结果)
  • 返回的是完整章节原文而非碎片片段,确保上下文完整
  • 候选池扩大到 top_k × 10(最多 500),保证多文献多章节的覆盖

POST /ask

问答端点(RAG)。 输入可以是远比关键词复杂的一段文本(包含多个问题、背景信息、病历描述等),系统会理解意图、拆分子问题、多次检索、最后用检索到的原文生成一个带引用角标的答案。

定位:输入是问题/描述,输出是AI 生成的答案 + 引用原文。每个事实、数字、推荐方案都带 [n] 角标,对应 citations 中的原文片段。

请求:

{
  "question": "我爷爷78岁确诊EGFR突变阳性晚期非小细胞肺癌,一线用什么药?如果奥希替尼耐药了怎么办?",
  "history": [
    { "question": "EGFR突变是什么?", "answer": "EGFR突变是..." }
  ],
  "top_k": 5,
  "guideline_id": null
}

| 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | question | string | 是 | 问题或描述(1-4000 字符,支持复杂输入) | | history | array | 否 | 历史对话(最多 2 轮,用于多轮对话上下文) | | history[].question | string | - | 历史用户问题 | | history[].answer | string | - | 历史 AI 回答 | | top_k | integer | 否 | 每个子问题的检索数(1-10,默认 5) | | guideline_id | string | 否 | 限定检索范围到单篇文献 |

处理流程:

  1. 意图理解 + 问题拆分:LLM 分析输入,识别真实意图,若包含多个独立问题则拆分为 1-5 个检索子问题
  2. 多路检索:每个子问题独立检索,结果按 (文献, 章节) 合并去重,最多取 12 条原文片段
  3. 带引用生成:LLM 基于编号的原文片段生成答案,强制要求每个事实/数字/推荐都带 [n] 角标

响应结构:

{
  "question": "我爷爷78岁确诊EGFR突变阳性晚期非小细胞肺癌...",
  "answer": "### 一、一线治疗\n\n推荐使用 EGFR-TKI 靶向药物 [1][2]...\n\n### 二、奥希替尼耐药后\n\n推荐含铂双药化疗 [3]...",
  "citations": [
    {
      "ref": 1,
      "text": "针对EGFR突变阳性晚期NSCLC一线治疗,多个随机对照研究显示...(原文片段)",
      "guideline_id": "9deec8c1ea024486b50bae9f708e8ef3",
      "guideline_title": "中国临床肿瘤学会(CSCO)原发性肺癌诊疗指南(2016.V1)",
      "publisher": "CSCO",
      "year": 2026,
      "section_path": "四、... > 5.IV 期驱动基因阳性... > 注释:",
      "citation_label": "中国临床肿瘤学会(CSCO)... > v1.0 > 四、... > 注释:",
      "source_pages": [28],
      "source_start": 45205,
      "score": 0.865
    }
  ],
  "sub_questions": [
    { "question": "EGFR突变阳性晚期非小细胞肺癌一线治疗推荐方案", "passage_count": 5 },
    { "question": "奥希替尼一线治疗后进展的二线治疗方案", "passage_count": 5 }
  ]
}

字段说明:

| 字段 | 说明 | |---|---| | answer | AI 生成的答案(Markdown 格式),其中 [n] 角标对应 citations 中的 ref | | citations | 引用来源列表,每个对象包含 ref(角标编号)、原文片段 text、文献元数据、source_start | | citations[].ref | 角标编号,与答案中的 [n] 一一对应 | | citations[].text | 被引用的原文片段(已清理,可直接展示) | | citations[].source_start | 该片段在文献正文中的字符偏移,可用于精确定位 | | sub_questions | 拆分出的子问题列表及各自检索到的片段数 |

角标使用:

答案中的 [1][2] 等角标对应 citations 数组中 ref 字段相同的项。前端可将角标渲染为可点击链接,点击后展开对应的原文片段,或跳转到文献原文页面(/review/{guideline_id}?sourceOffset={source_start})。

注意/ask 是同步接口,RAG 流程(拆分+检索+生成)可能耗时 30-60 秒。浏览器或经过代理的客户端建议使用下方的流式接口 /ask/stream 以避免超时。


POST /ask/stream

/askSSE 流式版本,请求参数完全一致,响应为 text/event-stream

推荐浏览器客户端使用此接口——既能避免代理超时(流式连接保持活跃),又能实现答案逐字输出的体验。

事件序列:

event: sub_questions
data: [{"question":"EGFR突变...一线治疗推荐方案","passage_count":0}, ...]

event: citations
data: [{"ref":1,"text":"...","guideline_id":"...","source_start":45205,...}, ...]

event: answer_delta
data: {"text":"根据指南推荐"}

event: answer_delta
data: {"text":",EGFR突变阳性"}

...(多次 answer_delta)

event: done
data: {}

| 事件 | data 内容 | 说明 | |---|---|---| | sub_questions | 子问题数组 | 拆分完成,开始检索 | | citations | 引用来源数组(全部候选片段) | 检索完成,ref 已编号 | | answer_delta | {"text":"增量文本"} | 答案逐段输出,需拼接 | | done | {} | 流结束 | | error | {"message":"..."} | 出错(流终止) |

前端接入示例(JavaScript):

const resp = await fetch('/api/public/v1/ask/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': '<your-key>',
    'Accept': 'text/event-stream',
  },
  body: JSON.stringify({ question: '...', top_k: 5 }),
});

const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let answer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  // SSE 事件以 \n\n 分隔
  let sep;
  while ((sep = buffer.indexOf('\n\n')) !== -1) {
    const rawEvent = buffer.slice(0, sep);
    buffer = buffer.slice(sep + 2);

    const lines = rawEvent.split('\n');
    let eventType = '', dataStr = '';
    for (const line of lines) {
      if (line.startsWith('event: ')) eventType = line.slice(7).trim();
      else if (line.startsWith('data: ')) dataStr += line.slice(6);
    }
    if (!eventType) continue;

    const data = JSON.parse(dataStr);
    if (eventType === 'answer_delta') {
      answer += data.text;
      // 更新 UI 显示 answer
    } else if (eventType === 'citations') {
      // 渲染引用列表
    } else if (eventType === 'done') {
      // 完成
    }
  }
}

GET /images/{path}

图片代理。从存储(MinIO)读取文献中的图片、表格截图。此端点无需鉴权。

associated_images[].url 返回相对路径,使用时需要用 API Base URL 拼接完整:

Base URL: https://onc.md/api/public/v1
Image URL: /api/public/v1/images/34c0458349ce4c7bb5ebbaa3e3f70224/v1/page_18_visual_fallback_1.png
完整地址: https://onc.md/api/public/v1/images/34c0458349ce4c7bb5ebbaa3e3f70224/v1/page_18_visual_fallback_1.png

响应带 Cache-Control: public, max-age=86400(缓存 24 小时)。


错误格式

所有错误统一返回:

{
  "error": {
    "code": "not_found",
    "message": "Guideline 'xxx' not found or not published",
    "details": null
  }
}

常见错误码:not_found(404)、image_not_found(404)、validation_error(422)、unauthorized(401)、forbidden(403)、qa_failed(500,问答处理失败)。


外部应用接入要点

  1. Base URL:找到你的部署地址,追加 /api/public/v1
  2. 获取 API Key:在后台「设置 → API 管理」创建一个 API Key
  3. 携带 Key:每次请求在 header 中带上 X-API-Key: <your-key>(图片代理除外)
  4. CORS 全开:浏览器 SPA 可直接 fetch(allow-origin: *
  5. 检索 vs 问答
    • 需要原文溯源 → 用 POST /search(输入关键词,返回原文段落)
    • 需要直接答案 → 用 POST /askPOST /ask/stream(输入问题,返回带引用的 AI 答案)
  6. 流式问答:浏览器客户端建议用 /ask/stream,避免代理超时,体验更好
  7. 引用溯源citation_label + source_pages + source_start 可精确追溯每条结果/引用的来源位置
  8. 图片处理associated_images[].url 返回的是相对路径(以 /api/public/v1/images/ 开头),调用方需要用 API Base URL 自行拼接完整
  9. LLM Agent 友好/searchagent_context.passages 专为 AI 拼接设计,配合 suggested_usage 使用