{"meta":{"title":"引文","intro":"引文将助手响应的跨度链接回支持它们的源。 创建或恢复会话时打开 enableCitations ，然后读取 citations 事件上的 assistant.message 有效负载，以呈现脚注、源列表或内联链接。","product":"GitHub Copilot","breadcrumbs":[{"href":"/zh/enterprise-cloud@latest/copilot","title":"GitHub Copilot"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos","title":"操作方法"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk","title":"Copilot SDK"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features","title":"功能"},{"href":"/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/citations","title":"引文"}],"documentType":"article"},"body":"# 引文\n\n引文将助手响应的跨度链接回支持它们的源。 创建或恢复会话时打开 enableCitations ，然后读取 citations 事件上的 assistant.message 有效负载，以呈现脚注、源列表或内联链接。\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\n> \\[!WARNING]\n> 引文是实验性的。 在将来的版本中，选项名称、事件有效负载和提供程序覆盖范围可能会更改。\n\n## 引文的工作原理\n\n引文由模型提供程序而不是 SDK 生成。 流有三个部分：\n\n1. 您的应用程序提供可引用材料，例如文档附件或包含源内容的工具结果。\n2. 当 `enableCitations` 开启时，运行时将该材料标记为可通过线路引用的。 对于 Anthropic 模型，文件附件作为启用了引文的 `document` 块发送。\n3. 模型返回引用元数据，运行时会在最终的 `citations` 事件中将其规范化为与提供程序无关的 `assistant.message` 对象。\n\n提供者支持有限。 每个来源记录上的 `provider` 字段会记录引文来自何处：\n\n| 提供者值        | Meaning                     |\n| ----------- | --------------------------- |\n| `anthropic` | 由Anthropic（Claude）模型响应生成的引文 |\n| `openai`    | OpenAI 模型响应生成的引文            |\n| `client`    | 运行时从工具输出合成的引文               |\n\n> \\[!NOTE]\n> `enableCitations`启用不保证响应包含引文。 只有当响应基于可引用的源材料时，模型才会输出这些内容。 始终将 `citations` 字段视为可选字段。\n\n## 在会话上启用引文\n\n在会话创建时设置选项，如果你想在重启后获得引文，请在恢复时再次设置。\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n<!-- docs-validate: skip -->\n\n```typescript\nconst session = await client.createSession({\n    onPermissionRequest: approveAll,\n    enableCitations: true,\n});\n\nconst resumed = await client.resumeSession(session.sessionId, {\n    onPermissionRequest: approveAll,\n    enableCitations: true,\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n<!-- docs-validate: skip -->\n\n```python\nsession = await client.create_session(\n    on_permission_request=PermissionHandler.approve_all,\n    enable_citations=True,\n)\n\nresumed = await client.resume_session(\n    session.session_id,\n    on_permission_request=PermissionHandler.approve_all,\n    enable_citations=True,\n)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n<!-- docs-validate: skip -->\n\n```golang\nsession, err := client.CreateSession(ctx, &copilot.SessionConfig{\n    OnPermissionRequest: copilot.PermissionHandler.ApproveAll,\n    EnableCitations:     copilot.Bool(true),\n})\n\nresumed, err := client.ResumeSession(ctx, session.SessionID, &copilot.ResumeSessionConfig{\n    OnPermissionRequest: copilot.PermissionHandler.ApproveAll,\n    EnableCitations:     copilot.Bool(true),\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n<!-- docs-validate: skip -->\n\n```csharp\nvar session = await client.CreateSessionAsync(new SessionConfig\n{\n    OnPermissionRequest = PermissionHandler.ApproveAll,\n    EnableCitations = true,\n});\n\nvar resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig\n{\n    OnPermissionRequest = PermissionHandler.ApproveAll,\n    EnableCitations = true,\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n<!-- docs-validate: skip -->\n\n```java\nCopilotSession session = client\n        .createSession(new SessionConfig()\n                .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n                .setEnableCitations(true))\n        .get();\n\nCopilotSession resumed = client\n        .resumeSession(session.getSessionId(), new ResumeSessionConfig()\n                .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n                .setEnableCitations(true))\n        .get();\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"rust\" data-label=\"Rust\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Rust</div>\n\n<!-- docs-validate: skip -->\n\n```rust\nlet session = client\n    .create_session(\n        SessionConfig::new()\n            .approve_all_permissions()\n            .with_enable_citations(true),\n    )\n    .await?;\n\nlet resumed = client\n    .resume_session(\n        ResumeSessionConfig::new(session.id().clone())\n            .approve_all_permissions()\n            .with_enable_citations(true),\n    )\n    .await?;\n```\n\n</div>\n\n</div>\n\n## 从助手消息读取引文\n\n引用出现在最终的 `assistant.message` 事件中，而不是在 `assistant.message_delta` 事件中。 在渲染源标记之前，先等待最终消息。\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n<!-- docs-validate: skip -->\n\n```typescript\nsession.on((event) => {\n    if (event.type !== \"assistant.message\" || !event.data.citations) {\n        return;\n    }\n\n    const { sources, spans } = event.data.citations;\n    const sourceById = new Map(sources.map((source) => [source.id, source]));\n\n    for (const span of spans) {\n        const quoted = event.data.content.slice(span.startIndex, span.endIndex);\n        for (const reference of span.references) {\n            const source = sourceById.get(reference.sourceId);\n            const label = source?.title ?? source?.url ?? source?.path ?? source?.id;\n            console.log(`\"${quoted}\" — ${label}`);\n        }\n    }\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n<!-- docs-validate: skip -->\n\n```python\nfrom copilot.session_events import SessionEventType\n\ndef utf16_slice(text: str, start: int, end: int) -> str:\n    \"\"\"Slice by UTF-16 code units, which is how span offsets are measured.\"\"\"\n    units = text.encode(\"utf-16-le\")\n    return units[start * 2 : end * 2].decode(\"utf-16-le\")\n\ndef handle(event):\n    if event.type != SessionEventType.ASSISTANT_MESSAGE or not event.data.citations:\n        return\n\n    sources = {source.id: source for source in event.data.citations.sources}\n\n    for span in event.data.citations.spans:\n        quoted = utf16_slice(event.data.content, span.start_index, span.end_index)\n        for reference in span.references:\n            source = sources[reference.source_id]\n            label = source.title or source.url or source.path or source.id\n            print(f'\"{quoted}\" — {label}')\n\nsession.on(handle)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n<!-- docs-validate: skip -->\n\n```golang\n// import \"unicode/utf16\"\n\nsession.On(func(event copilot.SessionEvent) {\n    d, ok := event.Data.(*copilot.AssistantMessageData)\n    if !ok || d.Citations == nil {\n        return\n    }\n\n    sources := map[string]copilot.CitationSource{}\n    for _, source := range d.Citations.Sources {\n        sources[source.ID] = source\n    }\n\n    // Span offsets are UTF-16 code units, so index the UTF-16 view of the content.\n    units := utf16.Encode([]rune(d.Content))\n\n    for _, span := range d.Citations.Spans {\n        quoted := string(utf16.Decode(units[span.StartIndex:span.EndIndex]))\n        for _, reference := range span.References {\n            source := sources[reference.SourceID]\n            label := source.ID\n            switch {\n            case source.Title != nil:\n                label = *source.Title\n            case source.URL != nil:\n                label = *source.URL\n            case source.Path != nil:\n                label = *source.Path\n            }\n            fmt.Printf(\"%q — %s\\n\", quoted, label)\n        }\n    }\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n<!-- docs-validate: skip -->\n\n```csharp\nsession.On<SessionEvent>(evt =>\n{\n    if (evt is not AssistantMessageEvent message || message.Data.Citations is null)\n    {\n        return;\n    }\n\n    var sources = message.Data.Citations.Sources.ToDictionary(source => source.Id);\n\n    foreach (var span in message.Data.Citations.Spans)\n    {\n        var quoted = message.Data.Content[(int)span.StartIndex..(int)span.EndIndex];\n        foreach (var reference in span.References)\n        {\n            var source = sources[reference.SourceId];\n            var label = source.Title ?? source.Url ?? source.Path ?? source.Id;\n            Console.WriteLine($\"\\\"{quoted}\\\" — {label}\");\n        }\n    }\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n<!-- docs-validate: skip -->\n\n```java\nsession.on(AssistantMessageEvent.class, event -> {\n    Citations citations = event.getData().citations();\n    if (citations == null) {\n        return;\n    }\n\n    Map<String, CitationSource> sources = citations.sources().stream()\n            .collect(Collectors.toMap(CitationSource::id, source -> source));\n\n    for (CitationSpan span : citations.spans()) {\n        String quoted = event.getData().content()\n                .substring(span.startIndex().intValue(), span.endIndex().intValue());\n        for (CitationReference reference : span.references()) {\n            CitationSource source = sources.get(reference.sourceId());\n            String label = source.title() != null ? source.title()\n                    : source.url() != null ? source.url()\n                    : source.path() != null ? source.path()\n                    : source.id();\n            System.out.printf(\"\\\"%s\\\" — %s%n\", quoted, label);\n        }\n    }\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"rust\" data-label=\"Rust\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Rust</div>\n\n<!-- docs-validate: skip -->\n\n```rust\nuse github_copilot_sdk::session_events::AssistantMessageData;\nuse std::collections::HashMap;\n\nlet mut events = session.subscribe();\n\nwhile let Ok(event) = events.recv().await {\n    if event.event_type != \"assistant.message\" {\n        continue;\n    }\n\n    let Some(data) = event.typed_data::<AssistantMessageData>() else {\n        continue;\n    };\n    let Some(citations) = data.citations.as_ref() else {\n        continue;\n    };\n\n    let sources: HashMap<&str, _> = citations\n        .sources\n        .iter()\n        .map(|source| (source.id.as_str(), source))\n        .collect();\n\n    // Span offsets are UTF-16 code units, so index the UTF-16 view of the content.\n    let units: Vec<u16> = data.content.encode_utf16().collect();\n\n    for span in &citations.spans {\n        let quoted = String::from_utf16_lossy(\n            &units[span.start_index as usize..span.end_index as usize],\n        );\n        for reference in &span.references {\n            let Some(source) = sources.get(reference.source_id.as_str()) else {\n                continue;\n            };\n            let label = source\n                .title\n                .as_deref()\n                .or(source.url.as_deref())\n                .or(source.path.as_deref())\n                .unwrap_or(source.id.as_str());\n            println!(\"\\\"{quoted}\\\" — {label}\");\n        }\n    }\n}\n```\n\n</div>\n\n</div>\n\n## 引文负载引用\n\n该 `citations` 对象将去重后的来源与引用这些来源的跨度分开，因此，被引用五次的某个来源在 `sources` 中只会出现一次。\n\n| 类型                  | 领域                  | Description                                   |\n| ------------------- | ------------------- | --------------------------------------------- |\n| `Citations`         | `sources`           | 由引文跨度引用的源的去重集                                 |\n| `Citations`         | `spans`             | 用其支持源注释的生成文本跨度                                |\n| `CitationSource`    | `id`                | 稳定的、由 `CitationReference.sourceId` 引用的轮次范围标识符 |\n| `CitationSource`    | `provider`          | 生成引用的系统：`anthropic`、`openai` 或 `client`       |\n| `CitationSource`    | `title?`            | 源的易读标题                                        |\n| `CitationSource`    | `url?`              | 源的 URL，当它是 Web 资源时                            |\n| `CitationSource`    | `path?`             | 当源是文件时，相对于智能体工作区根的文件路径                        |\n| `CitationSpan`      | `startIndex`        | 最终消息内容中的起始偏移量（UTF-16 代码单元，从零开始，包含）            |\n| `CitationSpan`      | `endIndex`          | 最终消息内容中的结束偏移量（UTF-16 代码单元，从零开始，不包含）           |\n| `CitationSpan`      | `references`        | 支持此跨度的来源                                      |\n| `CitationReference` | `sourceId`          | 此引用指向的 `CitationSource` 标识符                   |\n| `CitationReference` | `citedText?`        | 如果模型提供了该内容，则给出源文本中支持该片段的精确原文                  |\n| `CitationReference` | `location?`         | 源中支持跨度的位置                                     |\n| `CitationReference` | `providerMetadata?` | 提供方原生关联数据，以不透明方式传递                            |\n\n> \\[!TIP]\n> 跨度偏移量根据最终 `content` 字符串以 UTF-16 代码单元测量。 TypeScript、Java 和.NET字符串已是 UTF-16，因此可以直接对其进行切片。 Python字符串由 Unicode 代码点编制索引，Go 和 Rust 字符串为 UTF-8，因此在切片之前将内容转换为 UTF-16 代码单元，如上面的示例所示。\n\n### 引文位置\n\n`CitationReference.location` 是一个基于 `type` 的判别联合：\n\n| 位置类型                    | Fields       | Use |\n| ----------------------- | ------------ | --- |\n| `char`                  |              |     |\n| `startIndex`、`endIndex` | 源文本中的字符范围    |     |\n| `page`                  |              |     |\n| `startPage`、`endPage`   | 分页文档内的页面范围   |     |\n| `block`                 |              |     |\n| `startBlock`、`endBlock` | 结构化文档中的内容块范围 |     |\n\n## 提供可引用的来源\n\n引文需要模型可以属性的源材料。 有两种方法来提供它。\n\n### 将文档附加到邮件\n\n启用引用功能且会话使用 Anthropic 提供程序时，文件附件会以启用引用的 `document` 块形式发送，以便模型可以从中引用段落。\n\n<!-- docs-validate: skip -->\n\n```typescript\nawait session.sendAndWait({\n    prompt: \"Summarize the attached PDF and cite the passages you used.\",\n    attachments: [\n        {\n            type: \"blob\",\n            data: pdfBase64,\n            displayName: \"quarterly-report.pdf\",\n            mimeType: \"application/pdf\",\n        },\n    ],\n});\n```\n\n有关附件 API 以及 `blob` 和 [](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/image-input) 附件形状，请参阅 `file`。\n\n### 从工具返回可引用的来源\n\n工具结果包含一个实验性的 `citableSources` 数组。 每个条目都提供模型可引用的`content`，以及`id`和可选的`title`、`url`及`path`。 这些来源会与工具结果一同保存，因此在恢复会话后仍然可用，并且基于这些来源生成的引用会被标记为来自 `client` 提供程序。\n\n## 局限性\n\n* 引文在每个 SDK 中都是实验性的，不由兼容性保证涵盖。\n* 覆盖范围取决于模型提供方。 为没有引文支持的提供者配置的会话不会发出 `citations` 负载。\n* 引文仅出现在最终 `assistant.message` 事件上，因此流式处理使用者无法在响应中间呈现它们。\n* 公开代码和 IP 重复引用不属于此界面。\n\n## 延伸阅读\n\n* [流式处理会话事件](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/streaming-events)：订阅会话事件和缩小事件类型\n* [图像输入](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/image-input)：将文件和内存中的二进制大对象附加到消息中\n* [会话恢复和持久性](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/session-persistence)：恢复会话并重新应用会话选项\n* [SDK 和 CLI 兼容性](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/troubleshooting/compatibility)：SDK 和 CLI 功能矩阵"}