319 lines
13 KiB
Markdown
319 lines
13 KiB
Markdown
# Prompt Envelope Protocol
|
||
|
||
**让 LLM 上下文在聊天界面中可见、可读、可理解。**
|
||
|
||
---
|
||
|
||
## 问题:Prompt 被过度封装,用户不知道真正的输入是什么
|
||
|
||
以 ChatGPT、Claude 为代表的 AI 聊天产品已拥有数亿用户。这些产品的交互界面模拟的是「人对人」的对话——一个输入框,一段回复。用户看到自己打了一句话,AI 回了一句话。
|
||
|
||
**但模型真正收到的,远不止这句话。**
|
||
|
||
在用户输入的文本被发送给 LLM 之前,系统在后台拼接了大量的上下文:
|
||
|
||
- **System Prompt** —— 决定了回复的风格、边界、角色设定
|
||
- **User Memory** —— 跨对话持久化的用户画像,在每次请求时被注入
|
||
- **Tools & Skills 的声明** —— 函数签名、JSON Schema、触发条件的完整描述
|
||
- **模板变量的值** —— `{{user_name}}` 被替换为"小明",但用户看到的是变量名还是值?
|
||
|
||
对于普通用户,这可能无所谓。但对于需要精确控制模型行为的**进阶用户**(开发者、研究者、prompt engineer),这种不透明是致命的:
|
||
|
||
- 你精心设计了一段 prompt,但不知道 system prompt 是否和它冲突
|
||
- 你修改了一条 memory,但不确定新的值是否真的被注入了每一次请求
|
||
- 你添加了一个 tool,但看不到它的 schema 在上下文中以什么形式出现
|
||
- 你粘贴了一篇长论文,但不知道全文都被包含还是被截断
|
||
- 你看到 AI 回复了错误结果,但无法排查是哪个上下文片段导致了问题
|
||
|
||
**界面上看起来是「一句话的对话」,实际发送给模型的是一份由十几个组件拼接而成的结构化文档。** 聊天 UI 对这一切做了完全的封装——把复杂的 prompt 构造过程包装成了一个简单的 textarea。
|
||
|
||
**这个问题不是"看不到 AI 的推理过程",而是"看不到自己的输入到底是什么"。**
|
||
|
||
### 设计问题
|
||
|
||
当前的聊天界面将 prompt 构造的复杂性**过度封装**在单一输入框中。用户以为自己发送的是文本,实际发送的是一个经过层层拼接的复合体。界面模拟的是"人说了一句话"的隐喻,但模型侧的真实情况是"一份多来源、多类型、多优先级的结构化上下文文档"。
|
||
|
||
HCI 告诉我们:**界面的信息架构应该反映系统的心智模型**。当用户不知道模型真正收到了什么,他们就无法调试、无法精确控制、无法建立信任。
|
||
|
||
---
|
||
|
||
## 方案:Prompt Envelope Protocol
|
||
|
||
本项目的答案是两层设计:
|
||
|
||
- **协议层** —— 一种结构化的数据格式,用带类型的 Segment 描述 LLM 上下文
|
||
- **UI 层** —— 一个参考实现,展示协议如何被渲染为可读的聊天界面
|
||
|
||
### 协议:每条消息 = Segments 的有序列表
|
||
|
||
协议定义的不是"聊天文本",而是**上下文的结构化描述**。一条消息不再是字符串,而是若干带明确 `kind` 的 Segment:
|
||
|
||
```
|
||
一条消息不是 "你好,帮我审阅这篇论文"
|
||
而是:
|
||
├─ text segment "你好,帮我审阅这篇论文"
|
||
├─ long_text segment [论文全文,1423 字]
|
||
├─ document segment paper-draft.pdf (2.3MB)
|
||
└─ media segment image: 图3 实验组vs对照组
|
||
```
|
||
|
||
每个 Segment 的 `kind` 字段声明了"这是一段什么",其余字段携带数据本身。协议只关心 types 和 structure,**不关心颜色、折叠、布局**——那是 UI 层的决策。
|
||
|
||
### UI:每种类型有独立视觉呈现
|
||
|
||
在本项目的参考实现中,每种 Segment 被渲染为不同的视觉组件。例如 system prompt 是折叠面板,工具请求是代码块,变量在会话横栏中展示。**但这只是一种可能的 UI,协议本身与视觉无关。**
|
||
|
||
### 设计原则
|
||
|
||
**协议原则:**
|
||
|
||
| 原则 | 说明 |
|
||
|------|------|
|
||
| **类型明确** | 每种上下文有独立的 Segment kind,无一物混在文本中 |
|
||
| **可导出** | 协议可无损导出为标准 OpenAI Chat Format,不被本项目 UI 绑定 |
|
||
| **自描述** | 协议数据不依赖外部渲染逻辑——kind 字段即语义标签 |
|
||
|
||
**UI 原则(本项目的参考实现):**
|
||
|
||
| 原则 | 说明 |
|
||
|------|------|
|
||
| **信息密度梯度** | 核心对话文本优先可见;元信息默认折叠 |
|
||
| **颜色编码** | 不同类型用不同颜色辅助区分,形成可学习的视觉语言 |
|
||
| **折叠默认值在数据中** | segment 的 `collapsed` 字段指定默认状态,保证数据自足 |
|
||
|
||
---
|
||
|
||
## 协议:11 种 Segment 类型
|
||
|
||
通过对主流 LLM 聊天产品的逆向分析,我们识别出 LLM 上下文中 **11 种需要区分类型的信息**。每种在协议中对应一个 Segment kind。
|
||
|
||
### text
|
||
|
||
用户和 AI 的直接对话文本。协议中最基本的 Segment。
|
||
|
||
```typescript
|
||
{ kind: 'text', content: string }
|
||
```
|
||
|
||
### static_var
|
||
|
||
会话级模板变量,在对话开始时被替换为具体值。协议中携带变量名和展开后的值,使替换关系可见。
|
||
|
||
```typescript
|
||
{ kind: 'static_var', name: string, value: string, description?: string }
|
||
```
|
||
|
||
### system_prompt
|
||
|
||
模型的行为准则和角色设定。协议中为完整文本内容,可标记折叠默认状态。
|
||
|
||
```typescript
|
||
{ kind: 'system_prompt', content: string, collapsed: boolean }
|
||
```
|
||
|
||
### memory
|
||
|
||
跨对话持久化的用户信息,以标题-内容条目组织。协议中为记忆项列表。
|
||
|
||
```typescript
|
||
{ kind: 'memory', items: MemoryItem[], collapsed: boolean, description?: string }
|
||
```
|
||
|
||
### skills
|
||
|
||
模型可调用的技能,遵循 Anthropic SKILL.md 规范。协议中每项包含 name、description(L1 始终在上下文)和 body(L2 触发时加载的 Markdown 正文)。
|
||
|
||
```typescript
|
||
{ kind: 'skills', items: SkillItem[], collapsed: boolean, description?: string }
|
||
```
|
||
|
||
### tool_overview
|
||
|
||
模型可用的工具清单。每项包含名称、描述、参数摘要和可选的 JSON Schema 定义。
|
||
|
||
```typescript
|
||
{ kind: 'tool_overview', items: ToolItem[], collapsed: boolean }
|
||
```
|
||
|
||
### tool_call_request
|
||
|
||
模型发起的工具调用。协议中携带函数名和参数键值对。
|
||
|
||
```typescript
|
||
{ kind: 'tool_call_request', toolName: string, arguments: Record<string, unknown>, collapsed: boolean }
|
||
```
|
||
|
||
### tool_call_result
|
||
|
||
工具调用的执行结果。协议中标记成功/失败状态和结果文本。
|
||
|
||
```typescript
|
||
{ kind: 'tool_call_result', toolName: string, result: string, success: boolean, collapsed: boolean }
|
||
```
|
||
|
||
### document
|
||
|
||
用户上传的文件。协议中存储文件名、MIME 类型、大小和文本预览,可附带解析后的完整内容。
|
||
|
||
```typescript
|
||
{ kind: 'document', fileName: string, mimeType: string, snippet: string, sizeBytes: number, parsedContent?: string }
|
||
```
|
||
|
||
### long_text
|
||
|
||
用户粘贴的长篇文本素材。协议中携带完整内容和字符数,可标记折叠默认状态。
|
||
|
||
```typescript
|
||
{ kind: 'long_text', content: string, charCount: number, collapsed: boolean }
|
||
```
|
||
|
||
### media
|
||
|
||
图片、音频、视频等多模态内容。协议中记录媒体类型、URL 和替代文本。
|
||
|
||
```typescript
|
||
{ kind: 'media', mediaType: 'image'|'audio'|'video', url: string, altText?: string }
|
||
```
|
||
|
||
---
|
||
|
||
## UI 参考实现:差异化视觉呈现
|
||
|
||
以上是协议层——定义了什么信息存在、以什么结构存在。以下是本项目为协议做的参考 UI。**颜色、折叠行为、布局都是 UI 决策,不是协议规范。**
|
||
|
||
| Segment | UI 处理 | 交互 |
|
||
|---------|--------|------|
|
||
| `text` | 正文渲染,支持 Markdown | 无交互 |
|
||
| `static_var` | 提取到对话区顶部的会话变量横栏,不在气泡内 | 纯展示 |
|
||
| `system_prompt` | 折叠面板,灰色标记,标注行数 | 点击展开 |
|
||
| `memory` | 折叠面板,紫色标记,展开为标题-内容列表 | 点击展开 |
|
||
| `skills` | 折叠面板,绿色标记,每项支持渐进式披露(L1 名称+描述 → L2 body) | 逐层点击展开 |
|
||
| `tool_overview` | 折叠面板,橙色标记,每项可独立展开 JSON Schema | 点击展开 |
|
||
| `tool_call_request` | 深色终端风格代码块,参数以标签化键值行展示 | 默认展开;长值折叠 |
|
||
| `tool_call_result` | 绿色(成功)/ 红色(失败)状态条 | 成功默认折叠,失败默认展开 |
|
||
| `document` | 文件卡片:图标 + 文件名 + 大小 + 文本预览 | 可展开 AI 解析内容 |
|
||
| `long_text` | 折叠态前 2 行预览 + 字数标签 | 点击展开全文 |
|
||
| `media` | 图片有 URL 时可渲染缩略图;其他类型图标占位 | 加载失败回退为图标 |
|
||
|
||
完整类型定义见 [`src/types/protocol.ts`](src/types/protocol.ts)。
|
||
|
||
---
|
||
|
||
## 协议格式
|
||
|
||
### Prompt Envelope(顶层文档)
|
||
|
||
```typescript
|
||
interface PromptEnvelope {
|
||
version: '1.0'
|
||
model?: string // 导出时使用的模型名
|
||
messages: Message[]
|
||
}
|
||
```
|
||
|
||
### Message(单条消息)
|
||
|
||
```typescript
|
||
interface Message {
|
||
id: string
|
||
role: 'system' | 'user' | 'assistant' | 'tool'
|
||
segments: Segment[] // 有序的片段列表
|
||
timestamp: number
|
||
}
|
||
```
|
||
|
||
### Segment(片段联合类型)
|
||
|
||
每种 Segment 有独有的 `kind` 字段,标识其语义类型:
|
||
|
||
```
|
||
text | static_var | system_prompt | memory | skills |
|
||
tool_overview | tool_call_request | tool_call_result |
|
||
document | long_text | media
|
||
```
|
||
|
||
完整类型定义见 [`src/types/protocol.ts`](src/types/protocol.ts)。
|
||
|
||
### 导出兼容
|
||
|
||
协议可无损导出为标准 OpenAI Chat Completions 格式(`{ model, messages, tools }`),确认此设计不是"另一种 agent 运行时",而是一种**对标准协议的格式化表达层**。
|
||
|
||
---
|
||
|
||
## 技术实现
|
||
|
||
### 技术栈
|
||
|
||
| 层 | 选型 |
|
||
|---|------|
|
||
| 框架 | React 18 + TypeScript |
|
||
| 构建 | Vite |
|
||
| 样式 | Tailwind CSS |
|
||
| 图标 | Lucide React |
|
||
| Markdown | react-markdown + remark-gfm |
|
||
| 测试 | Vitest |
|
||
|
||
### MVP 范围
|
||
|
||
- ✅ 纯前端 React 应用,无后端、无真实 LLM 调用
|
||
- ✅ 6 个 Demo 场景(A–F),覆盖全部 Segment 类型
|
||
- ✅ 双栏布局:对话视图 + 协议面板(实时 JSON 预览)
|
||
- ✅ 双 Tab 协议面板:OpenAI Format / Raw Protocol,支持复制和下载
|
||
- ✅ 4 个真实 Anthropic SKILL.md 文件加载 + `parseSkillMarkdown()` 自研解析器
|
||
- ✅ 26 个单元测试覆盖导出逻辑和 SKILL.md 解析
|
||
- ❌ 不接入真实 LLM API
|
||
- ❌ 不做后端/数据库/用户认证
|
||
|
||
### 快速开始
|
||
|
||
```bash
|
||
git clone <this-repo>
|
||
cd hci
|
||
npm install
|
||
npm run dev # 启动开发服务器 → http://localhost:5173
|
||
npm test # 运行全部 26 个单元测试
|
||
npm run build # 生产构建
|
||
```
|
||
|
||
---
|
||
|
||
## 文件概览
|
||
|
||
```
|
||
src/
|
||
├── types/protocol.ts # 协议类型定义(11 种 Segment)
|
||
├── utils/
|
||
│ ├── export.ts # 导出 → OpenAI Chat Format
|
||
│ └── parseSkill.ts # SKILL.md YAML 解析器(自研)
|
||
├── data/
|
||
│ ├── demos/ # 6 个 Demo JSON 文件 + manifest
|
||
│ │ └── demos-loader.ts # 运行时验证 + skill body 水合
|
||
│ ├── skills.ts # 8 个手写自定义 skill
|
||
│ ├── skills-loader.ts # 4 个真实 Anthropic SKILL.md
|
||
│ └── skills/ # SKILL.md 源文件
|
||
├── components/
|
||
│ ├── SegmentRenderer.tsx # 按 kind 路由分发
|
||
│ ├── CollapsiblePanel.tsx # 通用折叠容器
|
||
│ ├── MarkdownRenderer.tsx # 共享 Markdown 渲染(所有文本组件)
|
||
│ ├── MessageList.tsx # 消息预处理 + 列表
|
||
│ ├── MessageBubble.tsx # 消息气泡容器
|
||
│ ├── SessionBar.tsx # 会话变量横栏
|
||
│ ├── ProtocolPanel.tsx # 右侧协议面板(双 Tab)
|
||
│ └── segments/ # 11 个 Segment View 组件
|
||
├── context/ChatContext.tsx # 全局状态
|
||
├── __tests__/
|
||
│ ├── export.test.ts # 导出逻辑(~20 个用例)
|
||
│ └── parseSkill.test.ts # SKILL.md 解析(7 个用例)
|
||
└── App.tsx # 顶层布局
|
||
```
|
||
|
||
---
|
||
|
||
## 设计反思
|
||
|
||
这个项目本质上在探索一个更根本的问题:**当一条"聊天消息"背后实际是十几个上下文组件拼接而成的结构化文档,聊天界面该怎样诚实地呈现它?**
|
||
|
||
传统的聊天界面诞生于"人对人"的隐喻——一个输入框,一个输出框。但当这个输入框背后连接的是一个拥有 system prompt、记忆、工具声明和变量注入的复杂 prompt 构造系统时,把一切封装在简单的 chat bubble 里就是在**对用户撒谎**。用户以为自己发送的是纯文本,实际被发送的是一份经过大量预处理的复合文档。
|
||
|
||
Prompt Envelope Protocol 尝试将"人机对话"的界面从**隐藏复杂性的 textarea** 升级为**诚实可见的信息架构**。让对话保持流畅,同时让 prompt 的真实组成变得可见。这不是一个关于"AI 在做什么"的设计——**这是关于"你到底对 AI 说了什么"的设计。**
|