用 Sciverse 做科学问答的 Citation Grounding
RAGAgent高级

用 Sciverse 做科学问答的 Citation Grounding

为 LLM 回答的每一句话找到可验证的文献来源,消除幻觉

用户场景
开发者构建高可信度科学问答系统,需要对 LLM 生成的每个论点进行事实核查,通过检索文献并验证原文来确认或标记为不可验证。
预估调用量
~10–25 次 API 调用
适用工具
agentic-searchcontent
调用链路
LLM 生成草稿→ 拆句→ agentic-search(逐句)→ content(验证原文)→ 标注引用

输入示例

LLM 生成的草稿回答:
"mRNA 疫苗使用可电离脂质纳米颗粒(iLNP)包裹 mRNA。其中 MC3 是最广泛使用的可电离脂质。LNP 的粒径通常在 80-100nm。"

输出示例

{
  "grounded_answer": "mRNA 疫苗使用可电离脂质纳米颗粒(iLNP)包裹 mRNA [1]。其中 MC3 是最广泛使用的可电离脂质 [2]。LNP 的粒径通常在 80-100nm [1]。",
  "citations": [
    {"id": 1, "doc_id": "lnp_review_2021", "offset": 4200, "quote": "iLNP encapsulates mRNA...", "verified": true},
    {"id": 2, "doc_id": "mc3_study_2018", "offset": 1800, "quote": "MC3 (DLin-MC3-DMA) remains the most widely...", "verified": true}
  ],
  "unverified_claims": []
}

Agent Prompt 示例

你是一个 Citation Grounding Agent。工作流程:
1. 接收 LLM 生成的草稿回答
2. 将草稿拆分为独立论点/句子
3. 对每个论点调用 agentic-search 查找支持证据
4. 对高分结果调用 content 读取原文,确认证据是否真正支持该论点
5. 输出每句话的来源(doc_id + offset + 原文引用),无法验证的标记为 [unverified]

分步实现

Step 1: 环境准备

安装依赖并配置环境变量

!pip install httpx
import os
os.environ["SCIVERSE_API_TOKEN"] = "sv-your-token-here"  # 替换为你的真实值

Step 2: 拆分草稿并逐句检索

将 LLM 回答拆分为独立论点,对每个论点调用 agentic-search

import os
import httpx

BASE = "https://api.sciverse.space"
TOKEN = os.environ["SCIVERSE_API_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}

def split_claims(draft: str) -> list:
    """将草稿拆分为独立论点句子"""
    sentences = [s.strip() for s in draft.split("。") if s.strip()]
    return [s for s in sentences if len(s) > 10]

async def search_evidence(claim: str):
    """对单个论点检索支持证据"""
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.post(
            f"{BASE}/agentic-search",
            headers=HEADERS,
            json={"query": claim, "top_k": 5}
        )
        resp.raise_for_status()
        return (resp.json().get("hits") or [])

draft = "mRNA 疫苗使用可电离脂质纳米颗粒(iLNP)包裹 mRNA。其中 MC3 是最广泛使用的可电离脂质。LNP 的粒径通常在 80-100nm。"
claims = split_claims(draft)
print(f"Split into {len(claims)} claims")

Step 3: 调用 content 验证原文

对高分 hit 调用 content 读取原文,确认是否真正支持论点

async def verify_with_content(hit: dict, claim: str) -> dict:
    """读取原文验证证据是否真正支持论点"""
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.get(
            f"{BASE}/content",
            headers=HEADERS,
            params={"doc_id": hit["doc_id"], "offset": hit.get("offset", 0), "limit": 1000}
        )
        resp.raise_for_status()
        data = resp.json()
        # 检查原文中是否包含与论点相关的关键词
        text = data["text"].lower()
        claim_keywords = [w for w in claim.lower().split() if len(w) > 3]
        match_count = sum(1 for kw in claim_keywords if kw in text)
        match_ratio = match_count / max(len(claim_keywords), 1)
        return {
            "doc_id": hit["doc_id"],
            "offset": hit.get("offset", 0),
            "quote": data["text"][:150],
            "match_ratio": match_ratio,
            "verified": match_ratio >= 0.3 and hit["score"] >= 0.7
        }

async def ground_claims(claims: list):
    results = []
    for claim in claims:
        hits = await search_evidence(claim)
        if hits and hits[0]["score"] >= 0.6:
            verification = await verify_with_content(hits[0], claim)
            results.append({"claim": claim, **verification})
        else:
            results.append({"claim": claim, "verified": False, "doc_id": None})
        status = "\\u2713" if results[-1]["verified"] else "\\u2717"
        print(f"  {status} {claim[:50]}...")
    return results

results = await ground_claims(claims)

Step 4: 生成带引用的最终回答

将验证结果组装为带 citation 的最终输出

def build_grounded_answer(results: list) -> dict:
    citations = []
    grounded_parts = []
    unverified = []

    for r in results:
        if r["verified"]:
            cite_id = len(citations) + 1
            citations.append({
                "id": cite_id,
                "doc_id": r["doc_id"],
                "offset": r.get("offset", 0),
                "quote": r.get("quote", ""),
                "verified": True
            })
            grounded_parts.append(f"{r['claim']} [{cite_id}]")
        else:
            grounded_parts.append(f"{r['claim']} [unverified]")
            unverified.append(r["claim"])

    return {
        "grounded_answer": "\\u3002".join(grounded_parts) + "\\u3002",
        "citations": citations,
        "unverified_claims": unverified
    }

final = build_grounded_answer(results)
print(f"\
Grounded answer:\
{final['grounded_answer']}")
print(f"\
Citations: {len(final['citations'])}")
print(f"Unverified: {len(final['unverified_claims'])}")
for c in final["citations"]:
    print(f"  [{c['id']}] {c['doc_id']} (offset: {c['offset']})")

注意事项

  • 仅靠 score 判定 verified 不够严谨;本示例增加了 content 原文验证步骤
  • match_ratio 关键词匹配仅为简化示例,生产环境建议使用 LLM/NLI 模型判断原文是否真正支持论点
  • 验证逻辑可根据需求增强:如使用 LLM 判断原文是否支持论点(NLI 任务)
  • score 阈值 0.7 是建议值,医学领域建议 0.8+
  • 生产环境建议并发验证多个 claims(asyncio.gather)以提升速度
  • 对于 unverified 的论点,建议在最终输出中明确标注或要求用户确认

FAQ

这个案例解决什么问题?

为 AI 生成的科学回答补充论文证据,降低无来源结论。

如何判断证据是否支持回答?

需要读取 content 上下文,并保留原文 quote 供人工或模型复核。

是否可以自动给每句话加引用?

可以辅助生成,但引用是否充分仍需根据原文上下文判断。

不支持的 claim 怎么处理?

应标记为未找到充分证据,而不是强行附会引用。

下一步

还没有 API Key?

登录控制台「密钥」即可创建。同一套 API Key 可用于已开通的 Sciverse、点石与 Skills 能力,提供基础试用额度,具体以账号权限为准。

前往控制台