编写语言模型提示词
你可以使用字符串拼接来构建语言模型提示词,但这很难组合功能并确保提示词保持在语言模型的上下文窗口内。为了克服这些限制,你可以使用 @vscode/prompt-tsx 库。
@vscode/prompt-tsx 库提供了以下功能
- 基于 TSX 的提示词渲染:使用 TSX 组件编写提示词,使其更具可读性和可维护性
- 基于优先级的修剪:自动修剪提示词中较不重要的部分,以使其适合模型的上下文窗口
- 灵活的 Token 管理:使用
flexGrow、flexReserve和flexBasis等属性来协同使用 Token 预算 - 工具集成:与 VS Code 的语言模型工具 API 进行集成
有关所有功能的完整概述和详细的使用说明,请参考 完整的 README。
本文介绍了使用该库进行提示词设计的实际示例。这些示例的完整代码可以在 prompt-tsx 仓库 中找到。
管理对话历史记录中的优先级
在提示词中包含对话历史记录非常重要,因为它使用户能够针对以前的消息提出后续问题。但是,你需要确保妥善处理其优先级,因为历史记录随时间推移可能会变得很大。我们发现最合理的模式通常是按以下顺序优先处理:
- 基础提示词指令
- 当前的用户查询
- 最近几轮的聊天历史记录
- 任何支持数据
- 尽可能多的剩余历史记录
因此,在提示词中将历史记录分为两部分,其中最近的提示词轮次优先于一般上下文信息。
在此库中,树中的每个 TSX 节点都有一个优先级,其概念类似于 zIndex,数字越大表示优先级越高。
第一步:定义 HistoryMessages 组件
要列出历史消息,请定义一个 HistoryMessages 组件。此示例提供了一个很好的起点,但如果处理更复杂的数据类型,你可能需要对其进行扩展。
本示例使用了 PrioritizedList 辅助组件,它会自动为其每个子级分配升序或降序的优先级。
import {
UserMessage,
AssistantMessage,
PromptElement,
BasePromptElementProps,
PrioritizedList,
} from '@vscode/prompt-tsx';
import { ChatContext, ChatRequestTurn, ChatResponseTurn, ChatResponseMarkdownPart } from 'vscode';
interface IHistoryMessagesProps extends BasePromptElementProps {
history: ChatContext['history'];
}
export class HistoryMessages extends PromptElement<IHistoryMessagesProps> {
render(): PromptPiece {
const history: (UserMessage | AssistantMessage)[] = [];
for (const turn of this.props.history) {
if (turn instanceof ChatRequestTurn) {
history.push(<UserMessage>{turn.prompt}</UserMessage>);
} else if (turn instanceof ChatResponseTurn) {
history.push(
<AssistantMessage name={turn.participant}>
{chatResponseToMarkdown(turn)}
</AssistantMessage>
);
}
}
return (
<PrioritizedList priority={0} descending={false}>
{history}
</PrioritizedList>
);
}
}
第二步:定义 Prompt 组件
接下来,定义一个包含基础指令、用户查询以及带有各自适当优先级的历史消息的 MyPrompt 组件。优先级值在同级之间是局部的。请记住,你可能希望在触及提示词中的其他任何内容之前修剪历史记录中的旧消息,因此你需要拆分两个 <HistoryMessages> 元素
import {
UserMessage,
PromptElement,
BasePromptElementProps,
} from '@vscode/prompt-tsx';
interface IMyPromptProps extends BasePromptElementProps {
history: ChatContext['history'];
userQuery: string;
}
export class MyPrompt extends PromptElement<IMyPromptProps> {
render() {
return (
<>
<UserMessage priority={100}>
Here are your base instructions. They have the highest priority because you want to make
sure they're always included!
</UserMessage>
{/* Older messages in the history have the lowest priority since they're less relevant */}
<HistoryMessages history={this.props.history.slice(0, -2)} priority={0} />
{/* The last 2 history messages are preferred over any workspace context you have below */}
<HistoryMessages history={this.props.history.slice(-2)} priority={80} />
{/* The user query is right behind the based instructions in priority */}
<UserMessage priority={90}>{this.props.userQuery}</UserMessage>
<UserMessage priority={70}>
With a slightly lower priority, you can include some contextual data about the workspace
or files here...
</UserMessage>
</>
);
}
}
现在,在库尝试修剪提示词的其他元素之前,所有较旧的历史消息都会被修剪。
第三步:定义 History 组件
为了使调用更轻松,请定义一个 History 组件,该组件包装历史消息并使用 passPriority 属性作为直通容器。通过 passPriority,其子级在优先级排序时将被视为包含元素的直接子级。
import { PromptElement, BasePromptElementProps } from '@vscode/prompt-tsx';
interface IHistoryProps extends BasePromptElementProps {
history: ChatContext['history'];
newer: number; // last 2 message priority values
older: number; // previous message priority values
passPriority: true; // require this prop be set!
}
export class History extends PromptElement<IHistoryProps> {
render(): PromptPiece {
return (
<>
<HistoryMessages history={this.props.history.slice(0, -2)} priority={this.props.older} />
<HistoryMessages history={this.props.history.slice(-2)} priority={this.props.newer} />
</>
);
}
}
现在,你可以使用并重复使用这个单一元素来包含聊天历史记录
<History history={this.props.history} passPriority older={0} newer={80}/>
扩展文件内容以适应大小
在此示例中,你希望在提示词中包含用户当前正在查看的所有文件的内容。这些文件可能会很大,以至于包含所有文件会导致其文本被修剪!此示例展示了如何使用 flexGrow 属性协同调整文件内容的大小,使其适合 Token 预算。
第一步:定义基础指令和用户查询
首先,你定义一个包含基础指令的 UserMessage 组件。
<UserMessage priority={100}>Here are your base instructions.</UserMessage>
然后,你通过使用 UserMessage 组件来包含用户查询。此组件具有高优先级,以确保它紧跟在基础指令之后被包含。
<UserMessage priority={90}>{this.props.userQuery}</UserMessage>
第二步:包含文件内容
现在,你可以使用 FileContext 组件来包含文件内容。为其分配值为 1 的 flexGrow,以确保它在基础指令、用户查询和历史记录之后渲染。
<FileContext priority={70} flexGrow={1} files={this.props.files} />
具有 flexGrow 值的元素在其传递给 render() 和 prepare() 调用的 PromptSizing 对象中会获得任何未使用的 Token 预算。你可以在 prompt-tsx 文档中阅读有关 flex 元素行为的更多信息。
第三步:包含历史记录
接下来,使用你之前创建的 History 组件包含历史消息。这会稍微棘手一些,因为你确实希望显示一些历史记录,但也希望文件内容占据提示词的大部分。
因此,为 History 组件分配值为 2 的 flexGrow,以确保它在包括 <FileContext /> 在内的所有其他元素之后渲染。但是,还要将 flexReserve 值设置为 "/5",以为历史记录保留总预算的 1/5。
<History
history={this.props.history}
passPriority
older={0}
newer={80}
flexGrow={2}
flexReserve="/5"
/>
第三步:组合提示词的所有元素
现在,将所有元素组合到 MyPrompt 组件中。
import {
UserMessage,
PromptElement,
BasePromptElementProps,
} from '@vscode/prompt-tsx';
import { History } from './history';
interface IFilesToInclude {
document: TextDocument;
line: number;
}
interface IMyPromptProps extends BasePromptElementProps {
history: ChatContext['history'];
userQuery: string;
files: IFilesToInclude[];
}
export class MyPrompt extends PromptElement<IMyPromptProps> {
render() {
return (
<>
<UserMessage priority={100}>Here are your base instructions.</UserMessage>
<History
history={this.props.history}
passPriority
older={0}
newer={80}
flexGrow={2}
flexReserve="/5"
/>
<UserMessage priority={90}>{this.props.userQuery}</UserMessage>
<FileContext priority={70} flexGrow={1} files={this.props.files} />
</>
);
}
}
第四步:定义 FileContext 组件
最后,定义一个 FileContext 组件,其中包含用户当前正在查看的文件的内容。因为你使用了 flexGrow,所以你可以通过使用 PromptSizing 中的信息来实现获取每个文件“有趣”行周围尽可能多行的逻辑。
为简起见,省略了 getExpandedFiles 的实现逻辑。你可以在 prompt-tsx 仓库中查看它。
import { PromptElement, BasePromptElementProps, PromptSizing, PromptPiece } from '@vscode/prompt-tsx';
class FileContext extends PromptElement<{ files: IFilesToInclude[] } & BasePromptElementProps> {
async render(_state: void, sizing: PromptSizing): Promise<PromptPiece> {
const files = await this.getExpandedFiles(sizing);
return <>{files.map(f => f.toString())}</>;
}
private async getExpandedFiles(sizing: PromptSizing) {
// Implementation details are summarized here.
// Refer to the repo for the complete implementation.
}
}
总结
在这些示例中,你创建了一个包含基础指令、用户查询、历史消息以及具有不同优先级的文件的 MyPrompt 组件。你使用了 flexGrow 来协同调整文件内容的大小,使其适合 Token 预算。
通过遵循此模式,你可以确保始终包含提示词中最重要的部分,同时根据需要修剪较不重要的部分以适合模型的上下文窗口。有关 getExpandedFiles 方法和 FileContextTracker 类的完整实现详细信息,请参考 prompt-tsx 仓库。