# 引文

引文将助理响应的跨度链接到支持它们的源。 创建或恢复会话时打开 enableCitations ，然后读取 citations 事件上的 assistant.message 有效负载，以呈现脚注、源列表或内联链接。

<!-- markdownlint-disable GHD046 GHD005 -->

<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->

> \[!WARNING]
> 引文是实验性的。 在将来的版本中，选项名称、事件有效负载和提供程序覆盖范围可能会更改。

## 引文的工作原理

引文由模型提供程序而不是 SDK 生成。 流有三个部分：

1. 您的应用程序提供可引用材料，例如文档附件或包含源内容的工具结果。
2. 当 `enableCitations` 启用时，运行时会将该内容标记为可在线上传输时引用。 对于 Anthropic 模型，文件附件会以启用引用的 `document` 块形式发送。
3. 模型返回引用元数据，运行时会在最终的 `citations` 事件中将其规范化为与提供程序无关的 `assistant.message` 对象。

提供程序支持有限。 每个来源记录上的 `provider` 字段会记录引文来自何处：

| 提供者值        | Meaning                     |
| ----------- | --------------------------- |
| `anthropic` | 由Anthropic（Claude）模型响应生成的引文 |
| `openai`    | OpenAI 模型响应生成的引文            |
| `client`    | 运行时从工具输出合成的引文               |

> \[!NOTE]
> `enableCitations`启用不保证响应包含引文。 只有当响应基于可引用的源材料时，模型才会输出这些内容。 始终将 `citations` 字段视为可选字段。

## 在会话上启用引文

在会话创建时设置选项，如果想要重启后引文，请在恢复时再次设置该选项。

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

<!-- docs-validate: skip -->

```typescript
const session = await client.createSession({
    onPermissionRequest: approveAll,
    enableCitations: true,
});

const resumed = await client.resumeSession(session.sessionId, {
    onPermissionRequest: approveAll,
    enableCitations: true,
});
```

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

<!-- docs-validate: skip -->

```python
session = await client.create_session(
    on_permission_request=PermissionHandler.approve_all,
    enable_citations=True,
)

resumed = await client.resume_session(
    session.session_id,
    on_permission_request=PermissionHandler.approve_all,
    enable_citations=True,
)
```

</div>

<div class="ghd-codetab" data-lang="go" data-label="Go"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Go</div>

<!-- docs-validate: skip -->

```golang
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
    OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
    EnableCitations:     copilot.Bool(true),
})

resumed, err := client.ResumeSession(ctx, session.SessionID, &copilot.ResumeSessionConfig{
    OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
    EnableCitations:     copilot.Bool(true),
})
```

</div>

<div class="ghd-codetab" data-lang="dotnet" data-label=".NET"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">.NET</div>

<!-- docs-validate: skip -->

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
    OnPermissionRequest = PermissionHandler.ApproveAll,
    EnableCitations = true,
});

var resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig
{
    OnPermissionRequest = PermissionHandler.ApproveAll,
    EnableCitations = true,
});
```

</div>

<div class="ghd-codetab" data-lang="java" data-label="Java"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Java</div>

<!-- docs-validate: skip -->

```java
CopilotSession session = client
        .createSession(new SessionConfig()
                .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
                .setEnableCitations(true))
        .get();

CopilotSession resumed = client
        .resumeSession(session.getSessionId(), new ResumeSessionConfig()
                .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
                .setEnableCitations(true))
        .get();
```

</div>

<div class="ghd-codetab" data-lang="rust" data-label="Rust"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Rust</div>

<!-- docs-validate: skip -->

```rust
let session = client
    .create_session(
        SessionConfig::new()
            .approve_all_permissions()
            .with_enable_citations(true),
    )
    .await?;

let resumed = client
    .resume_session(
        ResumeSessionConfig::new(session.id().clone())
            .approve_all_permissions()
            .with_enable_citations(true),
    )
    .await?;
```

</div>

</div>

## 从助理消息中读取引文

引用出现在最终的 `assistant.message` 事件中，而不是在 `assistant.message_delta` 事件中。 在渲染源标记之前，先等待最终消息。

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

<!-- docs-validate: skip -->

```typescript
session.on((event) => {
    if (event.type !== "assistant.message" || !event.data.citations) {
        return;
    }

    const { sources, spans } = event.data.citations;
    const sourceById = new Map(sources.map((source) => [source.id, source]));

    for (const span of spans) {
        const quoted = event.data.content.slice(span.startIndex, span.endIndex);
        for (const reference of span.references) {
            const source = sourceById.get(reference.sourceId);
            const label = source?.title ?? source?.url ?? source?.path ?? source?.id;
            console.log(`"${quoted}" — ${label}`);
        }
    }
});
```

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

<!-- docs-validate: skip -->

```python
from copilot.session_events import SessionEventType

def utf16_slice(text: str, start: int, end: int) -> str:
    """Slice by UTF-16 code units, which is how span offsets are measured."""
    units = text.encode("utf-16-le")
    return units[start * 2 : end * 2].decode("utf-16-le")

def handle(event):
    if event.type != SessionEventType.ASSISTANT_MESSAGE or not event.data.citations:
        return

    sources = {source.id: source for source in event.data.citations.sources}

    for span in event.data.citations.spans:
        quoted = utf16_slice(event.data.content, span.start_index, span.end_index)
        for reference in span.references:
            source = sources[reference.source_id]
            label = source.title or source.url or source.path or source.id
            print(f'"{quoted}" — {label}')

session.on(handle)
```

</div>

<div class="ghd-codetab" data-lang="go" data-label="Go"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Go</div>

<!-- docs-validate: skip -->

```golang
// import "unicode/utf16"

session.On(func(event copilot.SessionEvent) {
    d, ok := event.Data.(*copilot.AssistantMessageData)
    if !ok || d.Citations == nil {
        return
    }

    sources := map[string]copilot.CitationSource{}
    for _, source := range d.Citations.Sources {
        sources[source.ID] = source
    }

    // Span offsets are UTF-16 code units, so index the UTF-16 view of the content.
    units := utf16.Encode([]rune(d.Content))

    for _, span := range d.Citations.Spans {
        quoted := string(utf16.Decode(units[span.StartIndex:span.EndIndex]))
        for _, reference := range span.References {
            source := sources[reference.SourceID]
            label := source.ID
            switch {
            case source.Title != nil:
                label = *source.Title
            case source.URL != nil:
                label = *source.URL
            case source.Path != nil:
                label = *source.Path
            }
            fmt.Printf("%q — %s\n", quoted, label)
        }
    }
})
```

</div>

<div class="ghd-codetab" data-lang="dotnet" data-label=".NET"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">.NET</div>

<!-- docs-validate: skip -->

```csharp
session.On<SessionEvent>(evt =>
{
    if (evt is not AssistantMessageEvent message || message.Data.Citations is null)
    {
        return;
    }

    var sources = message.Data.Citations.Sources.ToDictionary(source => source.Id);

    foreach (var span in message.Data.Citations.Spans)
    {
        var quoted = message.Data.Content[(int)span.StartIndex..(int)span.EndIndex];
        foreach (var reference in span.References)
        {
            var source = sources[reference.SourceId];
            var label = source.Title ?? source.Url ?? source.Path ?? source.Id;
            Console.WriteLine($"\"{quoted}\" — {label}");
        }
    }
});
```

</div>

<div class="ghd-codetab" data-lang="java" data-label="Java"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Java</div>

<!-- docs-validate: skip -->

```java
session.on(AssistantMessageEvent.class, event -> {
    Citations citations = event.getData().citations();
    if (citations == null) {
        return;
    }

    Map<String, CitationSource> sources = citations.sources().stream()
            .collect(Collectors.toMap(CitationSource::id, source -> source));

    for (CitationSpan span : citations.spans()) {
        String quoted = event.getData().content()
                .substring(span.startIndex().intValue(), span.endIndex().intValue());
        for (CitationReference reference : span.references()) {
            CitationSource source = sources.get(reference.sourceId());
            String label = source.title() != null ? source.title()
                    : source.url() != null ? source.url()
                    : source.path() != null ? source.path()
                    : source.id();
            System.out.printf("\"%s\" — %s%n", quoted, label);
        }
    }
});
```

</div>

<div class="ghd-codetab" data-lang="rust" data-label="Rust"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Rust</div>

<!-- docs-validate: skip -->

```rust
use github_copilot_sdk::session_events::AssistantMessageData;
use std::collections::HashMap;

let mut events = session.subscribe();

while let Ok(event) = events.recv().await {
    if event.event_type != "assistant.message" {
        continue;
    }

    let Some(data) = event.typed_data::<AssistantMessageData>() else {
        continue;
    };
    let Some(citations) = data.citations.as_ref() else {
        continue;
    };

    let sources: HashMap<&str, _> = citations
        .sources
        .iter()
        .map(|source| (source.id.as_str(), source))
        .collect();

    // Span offsets are UTF-16 code units, so index the UTF-16 view of the content.
    let units: Vec<u16> = data.content.encode_utf16().collect();

    for span in &citations.spans {
        let quoted = String::from_utf16_lossy(
            &units[span.start_index as usize..span.end_index as usize],
        );
        for reference in &span.references {
            let Some(source) = sources.get(reference.source_id.as_str()) else {
                continue;
            };
            let label = source
                .title
                .as_deref()
                .or(source.url.as_deref())
                .or(source.path.as_deref())
                .unwrap_or(source.id.as_str());
            println!("\"{quoted}\" — {label}");
        }
    }
}
```

</div>

</div>

## 引用负载参考

该 `citations` 对象将去重后的来源与引用这些来源的跨度分开，因此，被引用五次的某个来源在 `sources` 中只会出现一次。

| 类型                  | 领域                  | Description                                      |
| ------------------- | ------------------- | ------------------------------------------------ |
| `Citations`         | `sources`           | 引文片段引用的去重后来源集合                                   |
| `Citations`         | `spans`             | 标注了其支持来源的生成文本片段                                  |
| `CitationSource`    | `id`                | 由 `CitationReference.sourceId` 引用的稳定的、限于当前轮次的标识符 |
| `CitationSource`    | `provider`          | 生成引用的系统：`anthropic`、`openai` 或 `client`          |
| `CitationSource`    | `title?`            | 源的易读标题                                           |
| `CitationSource`    | `url?`              | 源的 URL，当它是 Web 资源时                               |
| `CitationSource`    | `path?`             | 源为文件时，相对于代理工作区根目录的文件路径                           |
| `CitationSpan`      | `startIndex`        | 最终消息内容中的开始偏移量（UTF-16 代码单元，从零开始，含）                |
| `CitationSpan`      | `endIndex`          | 最终消息内容中的结束偏移量（UTF-16 代码单元、从零开始、独占）               |
| `CitationSpan`      | `references`        | 支持此跨度的来源                                         |
| `CitationReference` | `sourceId`          | 此引用所指向的 `CitationSource` 的标识符                    |
| `CitationReference` | `citedText?`        | 如果模型提供了该内容，则给出源文本中支持该片段的精确原文                     |
| `CitationReference` | `location?`         | 支持跨度的源中的位置                                       |
| `CitationReference` | `providerMetadata?` | 提供方原生关联数据，以不透明方式传递                               |

> \[!TIP]
> 跨度偏移量以 UTF-16 代码单位度量，相对于最终 `content` 字符串。 TypeScript、Java 和.NET字符串已是 UTF-16，因此可以直接对其进行切片。 Python字符串由 Unicode 代码点编制索引，Go 和 Rust 字符串为 UTF-8，因此在切片之前将内容转换为 UTF-16 代码单元，如上面的示例所示。

### 引文位置

`CitationReference.location` 是一个以 `type` 为判别键的可区分联合：

| 位置类型                    | Fields       | Use |
| ----------------------- | ------------ | --- |
| `char`                  |              |     |
| `startIndex`、`endIndex` | 源文本中的字符范围    |     |
| `page`                  |              |     |
| `startPage`、`endPage`   | 分页文档内的页面范围   |     |
| `block`                 |              |     |
| `startBlock`、`endBlock` | 结构化文档中的内容块范围 |     |

## 提供可引用的来源

引文需要模型可以属性的源材料。 有两种方法来提供它。

### 将文档附加到邮件

启用引用功能且会话使用 Anthropic 提供程序时，文件附件会以启用引用的 `document` 块形式发送，以便模型可以从中引用段落。

<!-- docs-validate: skip -->

```typescript
await session.sendAndWait({
    prompt: "Summarize the attached PDF and cite the passages you used.",
    attachments: [
        {
            type: "blob",
            data: pdfBase64,
            displayName: "quarterly-report.pdf",
            mimeType: "application/pdf",
        },
    ],
});
```

有关附件 API 以及 `blob` 和 [](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/image-input) 附件形状，请参阅 `file`。

### 从工具返回可引用的来源

工具结果包含一个实验性的 `citableSources` 数组。 每个条目都提供模型可引用的`content`，以及`id`和可选的`title`、`url`及`path`。 这些来源会与工具结果一同保存，因此在恢复会话后仍然可用，并且基于这些来源生成的引用会被标记为来自 `client` 提供程序。

## 局限性

* 引文在每个 SDK 中都是实验性的，不由兼容性保证涵盖。
* 覆盖范围取决于模型提供方。 为没有引文支持的提供程序配置的会话不会发出任何 `citations` 有效负载。
* 引文仅存在于最终 `assistant.message` 事件中，因此流式处理使用者不能在响应中呈现它们。
* 公开代码和 IP 重复引用不属于此界面。

## 延伸阅读

* [流式处理会话事件](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/streaming-events)：订阅会话事件和缩小事件类型
* [图像输入](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/image-input)：将文件和内存中的二进制大对象附加到消息中
* [会话恢复和持久性](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/session-persistence)：恢复会话并重新应用会话选项
* [SDK 和 CLI 兼容性](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/troubleshooting/compatibility)：SDK 和 CLI 功能矩阵