警告
引文是实验性的。 在将来的版本中,选项名称、事件有效负载和提供程序覆盖范围可能会更改。
引文的工作原理
引文由模型提供程序而不是 SDK 生成。 流有三个部分:
- 您的应用程序提供可引用材料,例如文档附件或包含源内容的工具结果。
- 当
enableCitations启用时,运行时会将该内容标记为可在线上传输时引用。 对于 Anthropic 模型,文件附件会以启用引用的document块形式发送。 - 模型返回引用元数据,运行时会在最终的
citations事件中将其规范化为与提供程序无关的assistant.message对象。
提供程序支持有限。 每个来源记录上的 provider 字段会记录引文来自何处:
| 提供者值 | Meaning |
|---|---|
anthropic | 由Anthropic(Claude)模型响应生成的引文 |
openai | OpenAI 模型响应生成的引文 |
client | 运行时从工具输出合成的引文 |
注意
enableCitations启用不保证响应包含引文。 只有当响应基于可引用的源材料时,模型才会输出这些内容。 始终将 citations 字段视为可选字段。
在会话上启用引文
在会话创建时设置选项,如果想要重启后引文,请在恢复时再次设置该选项。
const session = await client.createSession({
onPermissionRequest: approveAll,
enableCitations: true,
});
const resumed = await client.resumeSession(session.sessionId, {
onPermissionRequest: approveAll,
enableCitations: true,
});
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,
)
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),
})
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,
});
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();
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?;
从助理消息中读取引文
引用出现在最终的 assistant.message 事件中,而不是在 assistant.message_delta 事件中。 在渲染源标记之前,先等待最终消息。
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}`);
}
}
});
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)
// 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)
}
}
})
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}");
}
}
});
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);
}
}
});
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}");
}
}
}
引用负载参考
该 citations 对象将去重后的来源与引用这些来源的跨度分开,因此,被引用五次的某个来源在 sources 中只会出现一次。
| 类型 | 领域 | Description |
|---|---|---|
Citations | sources | 引文片段引用的去重后来源集合 |
Citations | spans | 标注了其支持来源的生成文本片段 |
CitationSource | id | 由 Citation 引用的稳定的、限于当前轮次的标识符 |
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? | 提供方原生关联数据,以不透明方式传递 |
提示
跨度偏移量以 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 块形式发送,以便模型可以从中引用段落。
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 和 附件形状,请参阅 file。
从工具返回可引用的来源
工具结果包含一个实验性的 citableSources 数组。 每个条目都提供模型可引用的content,以及id和可选的title、url及path。 这些来源会与工具结果一同保存,因此在恢复会话后仍然可用,并且基于这些来源生成的引用会被标记为来自 client 提供程序。
局限性
- 引文在每个 SDK 中都是实验性的,不由兼容性保证涵盖。
- 覆盖范围取决于模型提供方。 为没有引文支持的提供程序配置的会话不会发出任何
citations有效负载。 - 引文仅存在于最终
assistant.message事件中,因此流式处理使用者不能在响应中呈现它们。 - 公开代码和 IP 重复引用不属于此界面。
延伸阅读
- 流式处理会话事件:订阅会话事件和缩小事件类型
- 图像输入:将文件和内存中的二进制大对象附加到消息中
- 会话恢复和持久性:恢复会话并重新应用会话选项
- SDK 和 CLI 兼容性:SDK 和 CLI 功能矩阵