feat(editor): add Markdown rendering for ghost text and optimize prompt system

- Implement Markdown parsing for ghost text using Milkdown parser
- Add support for both block and inline content in ghost text
- Refactor prompt system with comprehensive rules and examples
- Adjust LLM parameters: increase temperature to 0.7, add repeat_penalty
- Add CSS styles for formatted ghost text (bold, italic, code, links)
- Add documentation for Copilot prompt system and ghost text rendering
This commit is contained in:
“ydy0615”
2026-02-13 21:17:45 +08:00
parent 65d4a57d33
commit 7cddfaba30
8 changed files with 966 additions and 30 deletions
+473
View File
@@ -0,0 +1,473 @@
# GitHub Copilot 提示词系统分析
## 概述
GitHub Copilot 的提示词系统是一个复杂的代码补全引擎,采用声明式组件架构来构建发送给 LLM 的提示词。本文档基于 `completions-sample-code/` 目录的源代码分析。
## 核心架构
```mermaid
flowchart TB
subgraph Input
A[用户光标位置] --> B[CompletionState]
C[当前文档] --> B
D[相似文件] --> B
end
subgraph PromptFactory
B --> E[VirtualPrompt]
E --> F[组件树构建]
end
subgraph Components
F --> G[CompletionsContext]
G --> H[DocumentMarker]
G --> I[Traits]
G --> J[Diagnostics]
G --> K[CodeSnippets]
G --> L[SimilarFiles]
G --> M[RecentEdits]
F --> N[CurrentFile]
end
subgraph Rendering
H --> O[CompletionsPromptRenderer]
I --> O
J --> O
K --> O
L --> O
M --> O
N --> O
O --> P[Prompt对象]
end
subgraph Output
P --> Q[API请求]
Q --> R[LLM补全]
end
```
## 1. 提示词基础配置
### 1.1 Token 限制
来源: [`prompt/src/prompt.ts`](../completions-sample-code/prompt/src/prompt.ts)
```typescript
// 最大补全长度
export const DEFAULT_MAX_COMPLETION_LENGTH = 500;
// 最大提示词长度 (模型上下文窗口 - 补全长度)
export const DEFAULT_MAX_PROMPT_LENGTH = 8192 - DEFAULT_MAX_COMPLETION_LENGTH;
// 默认代码片段数量
export const DEFAULT_NUM_SNIPPETS = 4;
// 后缀匹配阈值
export const DEFAULT_SUFFIX_MATCH_THRESHOLD = 10;
```
### 1.2 提示词分配比例
```typescript
export const DEFAULT_PROMPT_ALLOCATION_PERCENT = {
prefix: 35, // 光标前代码
suffix: 15, // 光标后代码
stableContext: 35, // 稳定上下文
volatileContext: 15 // 动态上下文
};
```
## 2. 语言标记系统
### 2.1 支持的语言
来源: [`prompt/src/languageMarker.ts`](../completions-sample-code/prompt/src/languageMarker.ts)
支持 60+ 种编程语言,每种语言定义了:
- `lineComment`: 单行注释标记 (start, end)
- `markdownLanguageIds`: Markdown 代码块语言标识符
示例:
```typescript
python: {
lineComment: { start: '#', end: '' },
markdownLanguageIds: ['python', 'py', 'gyp'],
},
javascript: {
lineComment: { start: '//', end: '' },
markdownLanguageIds: ['javascript', 'js'],
},
```
### 2.2 语言标记生成
```typescript
// 获取语言标记
export function getLanguageMarker(doc: DocumentInfo): string {
if (dontAddLanguageMarker.indexOf(languageId) === -1 && !hasLanguageMarker(doc)) {
if (languageId in shebangLines) {
return shebangLines[languageId]; // 如 #!/usr/bin/env python3
} else {
return `Language: ${languageId}`;
}
}
return '';
}
// 获取路径标记
export function getPathMarker(doc: DocumentInfo): string {
if (doc.relativePath) {
return `Path: ${doc.relativePath}`;
}
return '';
}
```
## 3. 组件系统架构
### 3.1 声明式组件
来源: [`prompt/src/components/components.ts`](../completions-sample-code/prompt/src/components/components.ts)
Copilot 使用类似 React 的 JSX 语法来声明提示词组件:
```typescript
// 基础组件类型
export type PromptElementProps<P = object> = P & Readonly<PromptAttributes & { children?: PromptComponentChildren }>;
// 组件上下文,提供状态管理
export interface ComponentContext {
useState<S>(initialState: S): [S, Dispatch<StateUpdater<S>>];
useData<T>(typePredicate: TypePredicate<T>, consumer: DataConsumer<T>): void;
}
```
### 3.2 默认提示词组件结构
来源: [`lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx`](../completions-sample-code/lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx)
```tsx
function defaultCompletionsPrompt(accessor: ServicesAccessor) {
return (
<>
<CompletionsContext>
<DocumentMarker tdms={tdms} weight={0.7} />
<Traits weight={0.6} />
<Diagnostics tdms={tdms} weight={0.65} />
<CodeSnippets tdms={tdms} weight={0.9} />
<SimilarFiles tdms={tdms} instantiationService={instantiationService} weight={0.8} />
<RecentEdits tdms={tdms} recentEditsProvider={recentEditsProvider} weight={0.99} />
</CompletionsContext>
<CurrentFile weight={1} />
</>
);
}
```
### 3.3 组件权重说明
| 组件 | 权重 | 说明 |
|------|------|------|
| RecentEdits | 0.99 | 最近编辑内容,最高优先级 |
| CodeSnippets | 0.9 | 代码片段 |
| SimilarFiles | 0.8 | 相似文件内容 |
| DocumentMarker | 0.7 | 文档标记(语言/路径) |
| Diagnostics | 0.65 | 诊断信息(错误/警告) |
| Traits | 0.6 | 代码特征 |
| CurrentFile | 1.0 | 当前文件内容(必须包含) |
## 4. 当前文件组件
来源: [`lib/src/prompt/components/currentFile.tsx`](../completions-sample-code/lib/src/prompt/components/currentFile.tsx)
### 4.1 光标前代码 (BeforeCursor)
```tsx
export function BeforeCursor(props: {
document: CompletionRequestDocument | undefined;
position: Position | undefined;
maxCharacters: number;
}) {
let text = props.document.getText({ start: { line: 0, character: 0 }, end: props.position });
if (text.length > props.maxCharacters) {
text = text.slice(-props.maxCharacters); // 截取最后 maxCharacters 字符
}
return <Text>{text}</Text>;
}
```
### 4.2 光标后代码 (AfterCursor)
```tsx
export function AfterCursor(props: {...}, context: ComponentContext) {
// 获取光标后所有文本
let suffix = props.document.getText({
start: props.position,
end: { line: Number.MAX_VALUE, character: Number.MAX_VALUE },
});
// 后缀缓存机制:使用编辑距离判断是否复用缓存
const dist = findEditDistanceScore(firstSuffixTokens.tokens, cachedSuffixTokens.tokens);
if (100 * dist < suffixMatchThreshold * tokens.length) {
suffixToUse = cachedSuffix; // 使用缓存的后缀
}
return <Text>{suffixToUse}</Text>;
}
```
## 5. 相似文件与代码片段
### 5.1 相似文件选择
来源: [`prompt/src/snippetInclusion/similarFiles.ts`](../completions-sample-code/prompt/src/snippetInclusion/similarFiles.ts)
```typescript
export interface SimilarFilesOptions {
snippetLength: number; // 代码片段长度(行数)
threshold: number; // 相似度阈值
maxTopSnippets: number; // 最大返回片段数
maxCharPerFile: number; // 每文件最大字符数
maxNumberOfFiles: number; // 最大文件数
maxSnippetsPerFile: number; // 每文件最大片段数
}
// 默认配置
export const defaultSimilarFilesOptions: SimilarFilesOptions = {
snippetLength: 60,
threshold: 0.0,
maxTopSnippets: 4,
maxCharPerFile: 10000,
maxNumberOfFiles: 20,
maxSnippetsPerFile: 1,
};
```
### 5.2 Jaccard 相似度匹配
来源: [`prompt/src/snippetInclusion/selectRelevance.ts`](../completions-sample-code/prompt/src/snippetInclusion/selectRelevance.ts)
```typescript
// 使用 Jaccard 相似度计算代码片段相关性
abstract class WindowedMatcher {
protected abstract similarityScore(a: Set<string>, b: Set<string>): number;
// 分词器:将代码转换为 token 集合
class Tokenizer {
tokenize(a: string): Set<string> {
return new Set(splitIntoWords(a).filter(x => !this.stopsForLanguage.has(x)));
}
}
}
```
### 5.3 代码片段格式化
来源: [`prompt/src/snippetInclusion/snippets.ts`](../completions-sample-code/prompt/src/snippetInclusion/snippets.ts)
```typescript
export function announceSnippet(snippet: SnippetToAnnounce) {
const headline = snippet.relativePath
? `Compare ${pluralizedSemantics} ${semantics} from ${snippet.relativePath}:`
: `Compare ${pluralizedSemantics} ${semantics}:`;
return { headline, snippet: snippet.snippet };
}
```
## 6. API 请求格式
### 6.1 请求结构
来源: [`lib/src/openai/fetch.ts`](../completions-sample-code/lib/src/openai/fetch.ts)
```typescript
type CompletionRequest = {
prompt: string; // 前缀代码
suffix: string; // 后缀代码
stream: true; // 始终使用流式响应
max_tokens: number; // 最大生成 token 数
n: number; // 并行补全数量
temperature: number; // 温度参数
top_p: number; // nucleus 采样参数
stop: string[]; // 停止标记
logprobs?: number; // logprob 数量
extra: {
language: string; // 语言 ID
trim_by_indentation?: boolean;
force_indent?: number;
next_indent?: number;
prompt_tokens: number;
suffix_tokens: number;
context?: string[]; // 额外上下文
};
};
```
### 6.2 停止标记
来源: [`lib/src/openai/openai.ts`](../completions-sample-code/lib/src/openai/openai.ts)
```typescript
const stopsForLanguage: { [key: string]: string[] } = {
markdown: ['\n\n\n'],
python: ['\ndef ', '\nclass ', '\nif ', '\n\n#'],
};
export function getStops(languageId?: string) {
return stopsForLanguage[languageId ?? ''] ?? ['\n\n\n', '\n```'];
}
```
### 6.3 温度参数
```typescript
export function getTemperatureForSamples(numShots: number): number {
if (numShots <= 1) return 0.0;
else if (numShots < 10) return 0.2;
else if (numShots < 20) return 0.4;
else return 0.8;
}
```
## 7. Tokenization
来源: [`prompt/src/tokenization/tokenizer.ts`](../completions-sample-code/prompt/src/tokenization/tokenizer.ts)
### 7.1 支持的 Tokenizer
```typescript
export enum TokenizerName {
cl100k = 'cl100k_base', // GPT-3.5/GPT-4
o200k = 'o200k_base', // GPT-4o
mock = 'mock', // 测试用
}
```
### 7.2 Tokenizer 接口
```typescript
export interface Tokenizer {
tokenLength(text: string): number;
tokenize(text: string): number[];
detokenize(tokens: number[]): string;
tokenizeStrings(text: string): string[];
takeLastTokens(text: string, n: number): { text: string; tokens: number[] };
takeFirstTokens(text: string, n: number): { text: string; tokens: number[] };
takeLastLinesTokens(text: string, n: number): string;
}
```
## 8. Tree-sitter 代码解析
来源: [`prompt/src/parse.ts`](../completions-sample-code/prompt/src/parse.ts)
### 8.1 支持的语言
```typescript
export enum WASMLanguage {
Python = 'python',
JavaScript = 'javascript',
TypeScript = 'typescript',
TSX = 'tsx',
Go = 'go',
Ruby = 'ruby',
CSharp = 'c-sharp',
Java = 'java',
Php = 'php',
Cpp = 'cpp',
}
```
### 8.2 用途
- 判断代码块是否为空块开始 (`isEmptyBlockStart`)
- 判断代码块是否完成 (`isBlockBodyFinished`)
- 获取语法节点起始位置 (`getNodeStart`)
## 9. 提示词构建流程
```mermaid
sequenceDiagram
participant User as 用户
participant VSCode as VS Code
participant GT as GhostText
participant PF as PromptFactory
participant Components as 组件系统
participant Tokenizer as Tokenizer
participant API as OpenAI API
User->>VSCode: 输入代码
VSCode->>GT: 请求补全
GT->>PF: extractPrompt
PF->>Components: 构建组件树
Components->>Components: DocumentMarker
Components->>Components: Traits
Components->>Components: Diagnostics
Components->>Components: CodeSnippets
Components->>Components: SimilarFiles
Components->>Components: RecentEdits
Components->>Components: CurrentFile
Components->>Tokenizer: 计算 token 数量
Tokenizer-->>Components: 返回 token 数
Components->>Components: Elision 省略处理
Components-->>PF: Prompt 对象
PF-->>GT: PromptResponse
GT->>API: 发送请求
API-->>GT: 流式返回补全
GT-->>VSCode: 显示 Ghost Text
VSCode-->>User: 展示建议
```
## 10. 关键设计模式
### 10.1 声明式组件
使用 JSX 语法声明提示词结构,支持:
- 组件组合
- 权重分配
- 状态管理
- 数据订阅
### 10.2 虚拟提示词树
在渲染前构建虚拟树结构,支持:
- 增量更新
- 高效 diff
- 条件渲染
### 10.3 Token 预算管理
- 每个组件有权重属性
- 根据 token 预算动态省略内容
- 优先保留高权重组件
### 10.4 后缀缓存
- 使用编辑距离判断后缀相似度
- 相似时复用缓存的后缀
- 减少 token 波动,提高缓存命中率
## 11. 实现参考
如果要在自己的项目中实现类似的提示词系统,需要关注以下核心模块:
1. **Tokenizer**: 使用 tiktoken 进行准确的 token 计数
2. **语言标记**: 为不同语言生成适当的标记
3. **上下文收集**: 收集相似文件、最近编辑等上下文
4. **Token 预算**: 动态分配 token 给不同组件
5. **FIM 格式**: 使用 Fill-In-the-Middle 格式发送请求
## 总结
GitHub Copilot 的提示词系统是一个精心设计的工程系统,核心特点包括:
1. **模块化组件架构**: 使用声明式组件构建提示词
2. **智能上下文选择**: 通过 Jaccard 相似度选择相关代码片段
3. **Token 预算管理**: 动态分配 token 给不同优先级的内容
4. **多语言支持**: 支持 60+ 种编程语言
5. **Tree-sitter 解析**: 精确理解代码结构
6. **流式响应**: 实时返回补全结果
+157
View File
@@ -0,0 +1,157 @@
# 虚拟文本 Markdown 渲染解决方案
## 问题分析
当前虚拟文本(灰色字)无法正确渲染 Markdown 和换行符,根本原因是:
1. **纯文本插入**`insertGhostText` 使用 `tr.insertText()` 直接插入纯文本
2. **绕过解析器**:文本未经过 Milkdown 的 Markdown 解析流程
3. **节点结构错误**`\n` 字符被当作普通字符,而非创建新段落节点
## 解决方案架构
```mermaid
flowchart TB
subgraph 当前流程
A1[LLM 返回 Markdown] --> B1[insertText 直接插入]
B1 --> C1[添加 copilot_ghost mark]
C1 --> D1[显示为灰色纯文本]
end
subgraph 新流程
A2[LLM 返回 Markdown] --> B2[调用 parserCtx 解析]
B2 --> C2[生成 ProseMirror 节点]
C2 --> D2[为所有节点添加 ghost 属性]
D2 --> E2[插入到文档]
E2 --> F2[显示为格式化灰色文本]
end
style D1 fill:#f99
style F2 fill:#9f9
```
## 技术方案
### 方案一:使用 Milkdown Parser 解析(推荐)
**优点**
- 完整支持 Markdown 语法
- 与编辑器行为一致
- 自动处理换行
**实现步骤**
1. 获取 `parserCtx` 从 Milkdown 上下文
2. 使用 parser 将 Markdown 解析为 ProseMirror Fragment
3. 遍历所有节点,添加 `copilot_ghost` mark
4. 使用 `tr.replaceWith()` 插入节点
### 方案二:使用 Decoration API(备选)
**优点**
- 不修改实际文档内容
- 更轻量级
**缺点**
- 实现复杂
- 可能与某些功能冲突
## 详细实现计划
### 步骤 1:修改 copilotPlugin.ts
需要修改以下部分:
```typescript
// 新增导入
import { parserCtx } from '@milkdown/kit/core'
// 修改 insertGhostText 函数
async function insertGhostText(view: EditorView, suggestion: string, from: number) {
if (!currentCtx || !suggestion) return
const schema = view.state.schema
const markType = schema.marks.copilot_ghost
if (!markType) return
// 使用 parser 解析 Markdown
const parser = currentCtx.get(parserCtx)
const doc = await parser(suggestion)
if (!doc) return
// 为所有文本节点添加 ghost mark
const ghostDoc = doc.descendants((node, pos) => {
if (node.isText) {
// 添加 mark
}
})
// 插入节点
const tr = view.state.tr
tr.replaceWith(from, from, ghostDoc.content)
tr.setMeta(COPILOT_PLUGIN_KEY, { from, to: from + doc.content.size, suggestion })
view.dispatch(tr)
}
```
### 步骤 2:处理换行符
换行符处理策略:
| 换行类型 | 处理方式 |
|---------|---------|
| 单个 `\n` | 创建 `hard_break` 节点 |
| 双个 `\n\n` | 创建新段落节点 |
| 列表项换行 | 创建新列表项节点 |
### 步骤 3:样式处理
需要修改 CSS 以支持格式化的虚拟文本:
```css
.copilot-ghost-text {
color: #999;
opacity: 0.6;
pointer-events: none;
}
/* 虚拟文本内的格式化元素 */
.copilot-ghost-text strong,
.copilot-ghost-text em,
.copilot-ghost-text code {
opacity: inherit;
}
```
### 步骤 4:状态管理
需要跟踪虚拟节点的范围,以便:
- Tab 键接受时正确移除 mark
- 用户输入时正确清除虚拟内容
- 导出时正确处理虚拟文本
## 文件修改清单
| 文件 | 修改内容 |
|------|---------|
| `src/plugins/copilotPlugin.ts` | 重构 insertGhostText,添加解析逻辑 |
| `src/components/MilkdownEditor.vue` | 更新 CSS 样式 |
| `src/plugins/types.ts` | 可能需要更新类型定义 |
## 风险与注意事项
1. **性能考虑**:解析 Markdown 可能有延迟,需要考虑用户体验
2. **嵌套处理**:复杂的 Markdown 结构(如嵌套列表)需要特殊处理
3. **撤销/重做**:确保虚拟文本的接受/拒绝正确处理 undo stack
4. **光标位置**:插入多段落内容后光标位置需要正确设置
## 验收标准
- [ ] Markdown 语法正确渲染(粗体、斜体、代码等)
- [ ] 换行符正确转换为段落
- [ ] Tab 键接受功能正常
- [ ] Escape 键拒绝功能正常
- [ ] 导出时虚拟文本正确处理
- [ ] 性能无明显下降