端到端流程:copilot 一次 AI 对话中 Prompt 是怎么注入的

源码版本: VS Code 1.112.0 + GitHub Copilot Chat 0.40.1
分析对象: extension.js(Copilot 扩展)+ extensionHostProcess.js(VS Code 核心)
文档定位: 专注”一次对话请求从用户输入到 LLM 调用”的 prompt 注入全过程


全局流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
用户在 Chat 面板输入 → 发送消息


┌──────────────────────────────────────────────────────────────────┐
│ VS Code 核心 (extensionHostProcess.js) │
│ │
│ ① Agent 发现 (IPromptsService) │
│ • 启动时/文件变化时,扫描所有注册路径的 .agent.md 文件 │
│ • 解析 YAML frontmatter → description, tools, model 等 │
│ • 注册为 Chat Participant(可通过 @name 调用) │
│ │
│ ② 用户消息路由 │
│ • 判断是否 @agent 调用 → 路由到对应 participant handler │
│ • 默认 → 路由到 Copilot 扩展的 main handler │
│ │
│ ③ Instructions 发现 (IPromptsService) │
│ • 扫描 .github/instructions/, ~/.copilot/instructions/ 等 │
│ • 通过 VS Code API 提供给扩展 │
│ │
│ ④ Skills 发现 │
│ • 扫描 .github/skills/, ~/.copilot/skills/ 等 │
│ • 匹配 SKILL.md 文件 │
└──────────────────────┬───────────────────────────────────────────┘
│ VS Code Extension API

┌──────────────────────────────────────────────────────────────────┐
│ Copilot 扩展 (extension.js) — 请求处理 │
│ │
│ ⑤ Instructions 收集 (CustomInstructionsService) │
│ • getAgentInstructions() │
│ → stat(.github/copilot-instructions.md) → 读取内容 │
│ • fetchInstructionsFromSetting() │
│ → 读 VS Code settings 中的 instructions 配置 │
│ │
│ ⑥ Prompt 树渲染 (PromptElement 体系) │
│ • SystemMessage: 基础 system prompt │
│ • Instructions 组件 (Fi): 合并所有 instructions │
│ • History 组件: 历史对话 │
│ • UserMessage: 当前用户消息 │
│ • ToolCallRounds: 工具调用结果 │
│ • 附件/代码片段/变量引用等 │
│ │
│ ⑦ Token 预算管理 — 根据模型上限裁剪 prompt 树 │
│ │
│ ⑧ 发送请求到 LLM API (CAPI / Azure / 直连) │
└──────────────────────────────────────────────────────────────────┘

第一阶段:Agent 发现与注册(启动时 + 实时监听)

1.1 发生时机

不是每次对话时才扫描,而是在 VS Code 启动时 + 文件变化时:

  • 启动时:IPromptsService 通过 findFiles() glob 模式扫描所有注册路径
  • 运行中:FileSystemWatcher 监听注册路径中的文件增删改,动态更新

1.2 扫描路径(直接来自源码 wj 数组)

1
2
3
4
5
6
7
// VS Code 核心 extensionHostProcess.js 中的定义
var wj = [
{path: ".github/agents", source: "github-workspace", storage: "local"},
{path: ".claude/agents", source: "claude-workspace", storage: "local"},
{path: "~/.claude/agents", source: "claude-personal", storage: "user"},
{path: "~/.copilot/agents", source: "copilot-personal", storage: "user"},
];

1.3 文件识别规则(Dj() 函数)

VS Code 核心通过 Dj() 函数判断每个发现的文件是什么类型:

1
2
3
4
5
6
7
8
9
10
function Dj(uri) {
let filename = basename(uri.path);

// 精确后缀匹配
if (filename.endsWith(".agent.md")) return "agent";

// 目录上下文推断 — agents 目录中的任意 .md(非 README)也视为 agent
if (filename.endsWith(".md") && filename !== "README.md" && isInAgentsDir(uri))
return "agent";
}

关键细节:agents 目录中不强制要求 .agent.md 后缀,任意 .md 文件(除 README.md)都会被识别为 agent。

1.4 YAML Frontmatter 解析

每个 agent 文件的 --- 包裹的 YAML header 被解析为元数据:

1
2
3
4
5
6
7
---
description: "..." # → 显示在 UI 中 + 用于语义匹配(决定是否自动触发)
tools: [...] # → 限制该 agent 可用的工具集
model: "..." # → 指定使用的模型
---

(正文内容 注入为 system prompt)

1.5 注册为 Chat Participant

发现的 agent 文件通过 $registerAgent() IPC 调用注册到 VS Code 的 Chat Participant 系统中。

注册后:

  • 用户可以通过 @agent-name 在对话中直接调用
  • 出现在 Chat 面板的 agent 选择列表中
  • Copilot 扩展可以通过 VS Code API 查询可用的 agents

第二阶段:用户发送消息 → 请求路由

2.1 用户消息进入

用户在 Chat 面板输入消息并发送(或通过 inline chat / terminal chat 等入口)。

2.2 路由决策

1
2
3
4
5
6
7
用户消息 "@llama-snapdragon-basic 编译项目"

├── 检测到 @agent 前缀 → 路由到该 agent 的 handler
│ └── agent .md 正文内容作为 system prompt

└── 无 @agent → 路由到 Copilot 默认 handler (main agent)
└── copilot-instructions.md 作为 instructions 注入

2.3 Subagent 模式(MANDATORY delegation)

当 main agent 收到消息但 copilot-instructions.md 中有 MANDATORY delegation 指令时:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户消息 "编译项目"(无 @ 前缀)


Main Agent 收到消息

├── 读取 copilot-instructions.md → 发现 MANDATORY delegation
│ "这些任务必须委托给 llama-snapdragon-basic"

├── 语义匹配 "编译项目" vs agent description 中的触发词
│ description: "...中文触发词:编译项目、构建项目..."

└── 调用 runSubagent(agentName: "llama-snapdragon-basic", prompt: "编译项目")
└── subagent 接管:以 llama-snapdragon-basic.agent.md 正文为 system prompt

第三阶段:Instructions 收集(Copilot 扩展内部)

这是 Copilot 扩展自己独立实现的逻辑,与 VS Code 核心并行。

3.1 CustomInstructionsService (类名 Nee)

这是 Copilot 内部的 instructions 管理服务,实现了 ICustomInstructionsService 接口。

构造函数中注册的 3 路匹配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Nee extends Disposable {
constructor(configurationService, envService, workspaceService,
fileSystemService, promptPathRepresentationService,
logService, extensionService, runCommandExecutionService) {

// 匹配器 1: 从 VS Code settings 中配置的自定义路径
this._matchInstructionLocationsFromConfig = lazy(() => {
// 读取 settings 中的路径列表
// 对每个路径,检查文件是否以 .instructions.md 结尾
});

// 匹配器 2: 从已安装扩展中注册的 chatInstructions
this._matchInstructionLocationsFromExtensions = lazy(() => {
// 遍历所有扩展的 packageJSON.contributes.chatInstructions
// 收集注册了 instructions 的目录
});

// 匹配器 3: Skills 目录中的 SKILL.md
this._matchInstructionLocationsFromSkills = lazy(() => {
// 合并用户级 (~/.copilot/skills 等) 和工作区级 (.github/skills 等) 路径
// 检查文件是否在这些目录下
});
}
}

3.2 getAgentInstructions() — 读取 copilot-instructions.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async getAgentInstructions() {
let results = [];

// 1. 检查 settings 开关
if (!this.configurationService.getConfig(UseInstructionFiles))
return results; // 开关关闭 → 跳过

// 2. 遍历所有工作区文件夹
for (let folder of this.workspaceService.getWorkspaceFolders()) {
try {
// 3. 拼接精确路径: {workspaceFolder}/.github/copilot-instructions.md
let uri = joinPath(folder, ".github/copilot-instructions.md");

// 4. stat() 检查文件是否存在(不缓存!每次调用都检查)
let stat = await this.fileSystemService.stat(uri);

// 5. 确认是文件(不是目录)
if (stat.type === FileType.File)
results.push(uri);

} catch {
// 文件不存在 → 静默跳过
}
}
return results; // 返回 URI 列表(不是内容,内容后续读取)
}

关键行为

  • 使用 stat() 而非 findFiles(),不走 glob 不走缓存
  • 每次 AI 请求都会调用,文件增删即时反映
  • 返回的是 URI 列表,实际读取在 Prompt 树渲染阶段

3.3 fetchInstructionsFromSetting() — 读取 settings 中的额外 instructions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async fetchInstructionsFromSetting(settingKey) {
let results = [], inlineInstructions = [];

// 检查 settings 的三个级别
let config = this.configurationService.inspectConfig(settingKey);

await this.collectInstructionsFromSettings([
config.workspaceFolderValue, // 工作区文件夹级
config.workspaceValue, // 工作区级
config.globalValue, // 全局用户级
], usedFiles, inlineInstructions, results);

return results;
}

3.4 collectInstructionsFromSettings() — 处理 settings 值

settings 支持两种格式的 instructions:

1
2
3
4
5
6
7
8
// settings.json 中的配置示例
"github.copilot.chat.codeGeneration.instructions": [
// 格式 1: 文件引用
{ "file": "coding-standards.md", "language": "typescript" },

// 格式 2: 内联文本
{ "text": "Always use 4 spaces for indentation" }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async collectInstructionsFromSettings(levels, usedFiles, inlineResults, fileResults) {
for (let level of levels) {
if (Array.isArray(level)) {
for (let item of level) {
// 格式 1: { file: "path", language?: "lang" }
if (isFileInstruction(item) && !usedFiles.has(item.file)) {
usedFiles.add(item.file);
await this._collectInstructionsFromFile(item.file, item.language, fileResults);
}
// 格式 2: { text: "直接文本", language?: "lang" }
if (isTextInstruction(item) && !usedTexts.has(item.text)) {
usedTexts.add(item.text);
inlineResults.push({ instruction: item.text, languageId: item.language });
}
}
}
}
}

3.5 readInstructionsFromFile() — 实际读取文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async readInstructionsFromFile(uri, languageId) {
try {
let bytes = await this.fileSystemService.readFile(uri);
let content = new TextDecoder().decode(bytes).trim();

if (!content) {
this.logService.debug(`Instructions file is empty: ${uri}`);
return;
}

return {
kind: 0, // 0 = 文件类型
content: [{ instruction: content, languageId: languageId }],
reference: uri
};
} catch {
this.logService.debug(`Instructions file not found: ${uri}`);
return;
}
}

3.6 isExternalInstructionsFile() — URI 来源判断

对于一个给定的文件 URI,判断它是否应该被当作 instructions 处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async isExternalInstructionsFile(uri) {
// 优先级从高到低

// 1. chat-internal scheme(内部生成)
if (uri.scheme === "vscode-chat-internal") return true;

// 2. vscodeUserData scheme 下 只认 .instructions.md ⚠️ 关键判断
if (uri.scheme === "vscode-userdata" && uri.path.endsWith(".instructions.md"))
return true;

// 3. settings 中配置的自定义路径
if (this._matchInstructionLocationsFromConfig.get()(uri))
return true;

// 4. 扩展注册的 instructions 路径
if (this._matchInstructionLocationsFromExtensions.get()(uri))
return true;

// 5. Skills 目录
if (this._matchInstructionLocationsFromSkills.get()(uri))
return true;

// 6. 扩展 prompt 文件
return this.isExtensionPromptFile(uri);
}

这就是为什么 User/prompts/ 下的 .agent.md 对 Copilot 无效
第 2 条规则明确只匹配 .instructions.md 后缀。


第四阶段:Prompt 树构建与渲染

Copilot 使用了一个声明式的 Prompt 树架构(类似 React 的组件树),每个 prompt 元素是一个 PromptElement 子类。

4.1 Prompt 树的顶层结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
┌──────────────── Prompt Root ────────────────┐
│ │
│ ┌─ SystemMessage (priority: 最高) ────────┐ │
│ │ "You are an AI programming assistant" │ │
│ │ + 模型/能力相关的基础 system prompt │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌─ Instructions 组件 (Fi) ──────────────────┐ │
│ │ ┌─ copilot-instructions.md 内容 ────┐ │ │
│ │ │ wrapped in <attachment> tag │ │ │
│ │ └───────────────────────────────────┘ │ │
│ │ ┌─ settings instructions ───────────┐ │ │
│ │ │ from CodeGeneration/TestGen/etc │ │ │
│ │ └───────────────────────────────────┘ │ │
│ │ ┌─ chat variable instructions ──────┐ │ │
│ │ │ from #instructions 引用 │ │ │
│ │ └───────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌─ Agent/Skills/Memory ─────────────────────┐ │
│ │ • Agent MD 正文 (if @agent 调用) │ │
│ │ • Skill 内容 (if skill 匹配) │ │
│ │ • Repo Memory (if enabled) │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌─ Context 组件 ────────────────────────────┐ │
│ │ • 打开的文件内容/选中的代码 │ │
│ │ • Workspace labels (项目技术栈识别) │ │
│ │ • Notebook 变量 │ │
│ │ • 引用的文件/符号 │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌─ History 组件 (priority: 900) ────────────┐ │
│ │ 历史对话轮次: │ │
│ │ • UserMessage + chatVariables │ │
│ │ • AssistantMessage │ │
│ │ • ToolCallRounds + results │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌─ UserMessage (当前轮) ────────────────────┐ │
│ │ 用户本次输入的消息 │ │
│ │ + 附件 (图片/文件/代码等) │ │
│ └───────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘

4.2 Instructions 组件 (Fi) 的渲染逻辑

Fi 是 Copilot 扩展中负责收集和渲染所有 instructions 的核心组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Fi extends PromptElement {
async render(state, context) {
let {
includeCodeGenerationInstructions, // 代码生成 instructions
includeTestGenerationInstructions, // 测试生成 instructions
includeCodeFeedbackInstructions, // 代码反馈 instructions
includeCommitMessageGenerationInstructions, // commit message instructions
includePullRequestDescriptionGenerationInstructions // PR description instructions
} = this.props;

let chunks = [];

// ─── 阶段 A: 收集 Chat Variable 中的 instructions ───
if (includeCodeGenerationInstructions !== false) {
if (this.props.chatVariables) {
for (let variable of this.props.chatVariables) {
if (isInstruction(variable)) {
if (isString(variable.value)) {
// 内联 instruction 文本
chunks.push(TextChunk(variable.value));
} else if (isUri(variable.value)) {
// 文件引用 → 读取并包装
let element = await this.createElementFromURI(variable.value);
if (element && !seen.has(element.content))
chunks.push(element.chuck);
}
}
}
}

// ─── 阶段 B: 收集 copilot-instructions.md ───
let agentInstructions = await this.customInstructionsService.getAgentInstructions();
for (let uri of agentInstructions) {
if (!seenUris.has(uri)) {
seenUris.add(uri);
let element = await this.createElementFromURI(uri);
if (element && !seen.has(element.content))
chunks.push(element.chuck);
}
}
}

// ─── 阶段 C: 收集 settings 中的 instructions ───
let settingsInstructions = [];
if (includeCodeGenerationInstructions !== false)
settingsInstructions.push(...await fetchInstructionsFromSetting(CodeGenerationInstructions));
if (includeTestGenerationInstructions)
settingsInstructions.push(...await fetchInstructionsFromSetting(TestGenerationInstructions));
if (includeCodeFeedbackInstructions)
settingsInstructions.push(...await fetchInstructionsFromSetting(CodeFeedbackInstructions));
// ... 类似的 CommitMessage 和 PullRequestDescription

for (let instruction of settingsInstructions) {
let element = this.createInstructionElement(instruction);
if (element) chunks.push(element);
}

// ─── 阶段 D: 组装最终输出 ───
if (chunks.length === 0) return; // 无 instructions → 不输出

return (
<>
"When generating code, please follow these user provided coding instructions."
{isMultiRoot && " This is a multi-root workspace..."}
" You can ignore an instruction if it contradicts a system message."
<br/>
<tag name="instructions">
...chunks
</tag>
</>
);
}
}

4.3 createElementFromURI() — 将文件 URI 转为 Prompt 元素

这个函数是所有 instruction 文件从磁盘到 prompt 的桥梁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
async createElementFromURI(uri, toolReferences) {
try {
// 1. 读取文件内容
let bytes = await this.fileSystemService.readFile(uri);
let content = new TextDecoder().decode(bytes);

// 2. 解析 tool references(如果有)
if (toolReferences?.length > 0)
content = await this.promptVariablesService.resolveToolReferencesInPrompt(content, toolReferences);

// 3. 构建属性(包含文件路径信息)
let attrs = { filePath: this.promptPathRepresentationService.getFilePath(uri) };

// 4. 多根工作区时,加上工作区名称
if (this.workspaceService.getWorkspaceFolders().length > 1) {
let folder = this.workspaceService.getWorkspaceFolder(uri);
if (folder)
attrs.workspaceFolder = this.workspaceService.getWorkspaceFolderName(folder);
}

// 5. 包装为 prompt 元素,用 <attachment> tag 标记
return {
chuck: (
<tag name="attachment" attrs={attrs}>
<references value={[new PromptReference(uri, content)]} />
<TextChunk>{content}</TextChunk>
</tag>
),
content: content // 用于去重
};
} catch {
this.logService.debug(`Instruction file not found: ${uri}`);
return;
}
}

4.4 最终注入到 LLM 的 prompt 结构示例

以一次典型对话为例,注入到 LLM API 的 messages 大致如下:

1
2
3
4
5
6
7
8
9
10
[
{
"role": "system",
"content": "You are an AI programming assistant...\n\n<instructions>\n\nWhen generating code, please follow these user provided coding instructions.\n\n<attachment filePath=\".github/copilot-instructions.md\">\n# Copilot Instructions for llama.cpp\n\n## Subagent Delegation (MANDATORY)\n> IMPORTANT: This repository targets Snapdragon...\n</attachment>\n\n</instructions>"
},
{
"role": "user",
"content": "编译项目"
}
]

第五阶段:Token 预算管理与裁剪

5.1 TokenLimit 元素

Prompt 树中的 TokenLimit 元素定义了子树的最大 token 上限:

1
2
3
4
5
<TokenLimit max={32768}>
<PrioritizedList priority={900} descending={false}>
{历史对话轮次...}
</PrioritizedList>
</TokenLimit>

5.2 裁剪策略

当总 token 超出模型限制时,按优先级裁剪:

  1. 优先保留: SystemMessage、Instructions、当前 UserMessage
  2. 可裁剪: 历史对话(从最早开始裁剪)、代码上下文(用 summarization 压缩)
  3. 智能压缩: 代码文件用 _computeSummarization() 方法折叠不重要的部分,插入 /* Lines X-Y omitted */ 注释

5.3 代码上下文的 summarization(Qxt 类)

1
2
3
4
5
6
_computeSummarization() {
// 1. 如果节点存活(与查询相关)且无子节点 → 保留原文
// 2. 如果节点存活但有子节点 → 递归处理子节点
// 3. 不存活的连续节点 → 替换为 "/* Lines X-Y omitted */"
// 4. 特殊处理: { ... } 块折叠为 { /* ... */ }
}

第六阶段:Code Review 请求中的 Instructions 注入

Code Review 有一个独立的 instructions 注入路径(函数 ioa),与对话中的 instructions 注入并行但独立:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
async function ioa(customInstructionsService, workspaceService, context, languageMap, startId) {
let guidelines = [];
let id = startId;

// 1. 收集 copilot-instructions.md
let uris = await customInstructionsService.getAgentInstructions();
for (let uri of uris) {
let instruction = await customInstructionsService.fetchInstructionsFromFile(uri);
if (instruction) {
let relativePath = workspaceService.asRelativePath(uri);
for (let content of instruction.content) {
// 语言过滤
if (content.languageId && !languageMap.has(content.languageId)) continue;
let filePatterns = content.languageId ? Array.from(languageMap.get(content.languageId)) : ["*"];

guidelines.push({
type: "github.coding_guideline",
id: String(id),
data: {
id: id,
type: "coding-guideline",
name: `Instruction from ${relativePath}`,
description: content.instruction,
filePatterns: filePatterns
}
});
id++;
}
}
}

// 2. 收集 settings 中的 instructions
let settingConfigs = [
{ config: CodeGenerationInstructions, name: "Code Generation Instruction" },
// ... 如果是 selection review 还包括 CodeFeedbackInstructions
];

for (let { config, name } of settingConfigs) {
let instructions = await customInstructionsService.fetchInstructionsFromSetting(config);
for (let instruction of instructions) {
for (let content of instruction.content) {
guidelines.push({
type: "github.coding_guideline",
// ... 同上
});
id++;
}
}
}

return guidelines;
}

注入格式:instructions 被包装为 coding_guideline 类型,与对话中的 <instructions> tag 包装不同。


第七阶段:Remote Agent(远程 Agent)

Copilot 还支持从 GitHub 服务端加载远程 agent:

1
2
3
4
5
6
7
8
9
10
11
12
// 从 CAPI 获取远程 agent 列表
let response = await capiClientService.makeRequest({
method: "GET",
headers: { Authorization: `Bearer ${token}` }
}, { type: RemoteAgent });

let agents = JSON.parse(response).agents;

// 注册每个远程 agent
for (let agent of agents) {
_oe.set(agent.slug, this.registerAgent(agent));
}

远程 agent 需要用户授权,授权状态存储在 globalState

1
2
let key = `copilot.agent.${slug}.authorized`;
// 检查 globalState 或 workspaceState

第八阶段:Workspace Labels(工作区技术栈识别)

Copilot 会自动检测项目的技术栈,作为上下文信息注入 prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class BasicWorkspaceLabels {
initIndicators() {
this.addIndicator("package.json", "javascript", "npm");
this.addIndicator("tsconfig.json", "typescript");
this.addIndicator("CMakeLists.txt", "c++", "cmake");
this.addIndicator("requirements.txt","python", "pip");
this.addIndicator("Cargo.toml", "rust", "cargo");
this.addIndicator("go.mod", "go", "go.mod");
// ... 更多指标文件
}

// 还会解析文件内容获取更精确的信息
collectCMakeListsTxtIndicators(content) {
// 从 CMAKE_CXX_STANDARD 提取 C++ 版本: C++17, C++20 等
// 从 CMAKE_C_STANDARD 提取 C 版本
}

collectPackageJsonIndicators(content) {
// 从 dependencies 识别: angular, react, vue 等
// 从 engines 识别: node, vscode extension
}
}

第九阶段:Agent Memory(仓库记忆)

如果启用了 Copilot Memory,还会从 GitHub 服务端获取仓库级别的记忆信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async getRepoMemories(limit = 10) {
// 1. 检查是否启用
if (!await this.checkMemoryEnabled()) return;

// 2. 获取仓库 NWO (Name With Owner)
let nwo = await this.getRepoNwo(); // e.g. "dx2331lxz/mllama"

// 3. 调用 CAPI API
let response = await capiClientService.makeRequest({
method: "GET",
headers: { Authorization: `Bearer ${token}` }
}, { type: CopilotAgentMemory, repo: nwo, action: "recent", limit: limit });

// 4. 解析为 memory 对象
let memories = response.json()
.filter(isValidMemory)
.map(m => ({
subject: m.subject, // 记忆主题
fact: m.fact, // 记忆内容
citations: m.citations,// 引用来源
reason: m.reason, // 记忆原因
category: m.category // 分类
}));

return memories;
}

Memory 同样会被注入到 prompt 中,帮助 LLM 在后续对话中”记住”仓库的惯例和上下文。


完整调用时序图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
时间线 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→

[VS Code 启动]

├─ IPromptsService 初始化
│ ├─ findFiles(".github/agents/*.md") → 发现 agent 文件
│ ├─ findFiles("~/.copilot/agents/*.md") → 发现用户级 agent
│ ├─ 解析 YAML frontmatter
│ └─ $registerAgent() → 注册为 Chat Participant

├─ FileSystemWatcher 启动
│ └─ 监听 .github/agents/, ~/.copilot/agents/ 等目录

└─ WorkspaceLabels 收集
└─ 扫描 package.json, CMakeLists.txt 等 → 识别技术栈

[用户发送消息]

├─ 路由判断: @agent 调用 or 默认 handler

├─ Copilot 扩展接管
│ │
│ ├─ CustomInstructionsService.getAgentInstructions()
│ │ └─ stat(".github/copilot-instructions.md") → 存在 → 返回 URI
│ │
│ ├─ CustomInstructionsService.fetchInstructionsFromSetting()
│ │ └─ 读 settings 三个级别: folder → workspace → global
│ │
│ ├─ AgentMemoryService.getRepoMemories() (if enabled)
│ │ └─ GET /copilot-agent-memory → 获取仓库记忆
│ │
│ └─ Prompt 树渲染
│ ├─ SystemMessage: 基础 system prompt
│ ├─ Fi (Instructions): 合并所有 instructions
│ │ ├─ readFile(copilot-instructions.md) → 读取内容
│ │ ├─ 包装为 <attachment filePath="..."> tag
│ │ └─ settings instructions → 包装为 TextChunk
│ ├─ History: 历史对话轮次
│ ├─ UserMessage: 当前消息
│ └─ 代码上下文 / 附件 / 变量引用

├─ Token 预算裁剪
│ └─ 超出限制 → 按优先级裁剪历史 → 压缩代码上下文

└─ 发送 HTTP 请求到 LLM API
└─ messages: [system, ...history, user]

关键 Settings 开关对注入流程的影响

Setting 默认值 影响
github.copilot.chat.codeGeneration.useInstructionFiles true 关闭 → getAgentInstructions() 直接返回空,copilot-instructions.md 不被读取
chat.useAgentsMdFile true 关闭 → .agent.md 文件不被 VS Code 核心发现
chat.useAgentSkills true 关闭 → Skills 目录不被扫描
chat.agentFilesLocations (默认 4 路径) 自定义 → 可添加或排除 agent 扫描路径
chat.instructionsFilesLocations (默认 4 路径) 自定义 → 可添加或排除 instructions 扫描路径

踩过的坑与注意事项

1. stat() vs FileSystemWatcher

  • copilot-instructions.md每次请求 stat() 检查,即时生效
  • .agent.md 文件:FileSystemWatcher 监听,理论上即时生效,但极端情况可能需要几秒延迟

2. 去重逻辑

Instructions 组件 (Fi) 在渲染时做了双重去重:

  • seenUris(Set):按 URI 去重,同一文件不重复读取
  • seen(Set):按内容去重,不同路径但相同内容也只注入一次

3. 语言过滤

Code Review 的 instructions 注入(ioa 函数)支持按编程语言过滤:

  • 如果 instruction 指定了 languageId,只对匹配语言的文件生效
  • 如果未指定 languageId,则匹配所有文件(filePatterns: ["*"]

4. 多根工作区

多根工作区时:

  • getAgentInstructions()遍历所有工作区文件夹,每个文件夹都独立检查
  • createElementFromURI() 会在 <attachment> tag 中额外标注 workspaceFolder 属性
  • Instructions 组件会添加提示:”This is a multi-root workspace. Apply each set of instructions to the folder it belongs to.”

5. 冲突处理

当用户 instructions 与系统消息矛盾时,Fi 组件会默认添加提示:

1
"You can ignore an instruction if it contradicts a system message."

这意味着系统消息的优先级高于用户 instructions