경고
인용은 실험적입니다. 옵션 이름, 이벤트 페이로드 및 공급자 적용 범위는 향후 릴리스에서 변경될 수 있습니다.
인용 작동 방식
인용은 SDK가 아닌 모델 공급자에 의해 생성됩니다. 흐름에는 다음 세 부분이 있습니다.
- 애플리케이션은 원본 콘텐츠를 전달하는 문서 첨부 파일 또는 도구 결과와 같은 인용 가능한 자료를 제공합니다.
- 런타임은
enableCitations가 활성화되면 해당 자료를 전송 시 인용 가능한 상태로 표시합니다. Anthropic 모델의 경우 파일 첨부는 인용이 활성화된document블록으로 전송됩니다. - 모델은 인용 메타데이터를 반환하고 런타임은 최종
assistant.message이벤트에서 공급자 중립적citations개체로 정규화합니다.
공급자 지원은 제한됩니다.
provider 각 원본 레코드의 필드는 인용의 출처를 기록합니다.
| 공급자 값 | 의미 |
|---|---|
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_delta최종 이벤트에서 도착하며, assistant.message이벤트에서는 도착하지 않습니다. 원본 마커를 렌더링하기 전에 마지막 메시지를 기다립니다.
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 객체는 중복 제거된 소스와 이를 참조하는 span을 분리하므로, 다섯 번 인용된 소스도 sources에는 한 번만 나타납니다.
| Type | Field | Description |
|---|---|---|
Citations | sources | 인용 구간에서 참조된 중복 제거 출처 집합 |
Citations | spans | 이를 뒷받침하는 출처로 주석 처리된 생성된 텍스트 범위 |
CitationSource | id | |
Citation에서 참조되는 안정적인 턴 범위 식별자 | ||
CitationSource | provider | 인용을 생성한 시스템: anthropic, openai또는 client |
CitationSource | title? | 사람이 읽을 수 있는 원본 제목 |
CitationSource | url? | 원본의 URL(웹 리소스인 경우) |
CitationSource | path? | 원본이 파일인 경우 에이전트 작업 영역 루트를 기준으로 하는 파일 경로 |
CitationSpan | startIndex | 최종 메시지 콘텐츠의 시작 오프셋(UTF-16 코드 단위, 0부터 시작, 포함) |
CitationSpan | endIndex | 최종 메시지 콘텐츠의 끝 오프셋(UTF-16 코드 단위, 0 기반, 미포함) |
CitationSpan | references | 이 구간을 뒷받침하는 출처들 |
CitationReference | sourceId | 이 참조가 가리키는 CitationSource의 식별자 |
CitationReference | citedText? | 모델에서 해당 범위를 제공한 경우, 그 범위를 뒷받침하는 원본의 정확한 텍스트 |
CitationReference | location? | 해당 범위를 뒷받침하는 원본 내 위치 |
CitationReference | providerMetadata? | 불투명하게 전달된 공급자 네이티브 상관 관계 데이터 |
팁
범위 오프셋은 최종 content 문자열에 대해 UTF-16 코드 단위로 측정됩니다. TypeScript, Java 및 .NET 문자열은 이미 UTF-16이므로 직접 조각화할 수 있습니다. Python 문자열은 유니코드 코드 포인트에 의해 인덱싱되고 Go 및 Rust 문자열은 UTF-8이므로 위의 예제와 같이 조각화하기 전에 콘텐츠를 UTF-16 코드 단위로 변환합니다.
인용 위치
CitationReference.location는 type를 판별 키로 사용하는 식별된 유니온입니다:
| 위치 유형 | Fields | 사용하세요 |
|---|---|---|
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, 그리고 선택 사항인 path, title, url를 함께 제공합니다. 이러한 소스는 도구 결과와 함께 저장되므로 세션을 다시 시작해도 유지되며, 이를 기반으로 생성된 인용은 client 공급자로 태그됩니다.
Limitations
- 인용은 모든 SDK에서 실험적이며 호환성 보장에 포함되지 않습니다.
- 적용 범위는 모델 공급자에 따라 달라집니다. 인용을 지원하지 않는 공급자에 대해 구성된 세션은
citations페이로드를 내보내지 않습니다. - 인용은 최종
assistant.message이벤트에만 존재하므로 스트리밍 소비자는 중간 응답을 렌더링할 수 없습니다. - 공용 코드 및 IP 중복 인용은 이 화면의 일부가 아닙니다.
추가 읽기
- 스트리밍 세션 이벤트: 세션 이벤트 구독 및 이벤트 유형 좁히기
- 이미지 입력: 메시지에 파일 및 메모리 내 Blob 연결
- 세션 다시 시작 및 지속성: 세션 다시 시작 및 세션 옵션 다시 적용
- SDK 및 CLI 호환성: SDK 및 CLI 기능 매트릭스