openinterpreter 会话标题自动生成机制:opencode 标题提示词设计与源码调用链全解析
本篇聚焦 openinterpreter(Codex 核心,codex-rs)中 opencode 聊天适配层(chat harness)专用的标题生成系统提示词 opencode_title_prompt.md:它如何通过一组任务契约、规则约束和少样本示例约束模型输出“可检索的会话标题”,以及在源码中何时被触发、如何被装配进真实的 Chat Completions 请求并先行发送。读完你可以完整复现这条标题生成链路的调用关系,并掌握这类“纯输出型子任务提示词”的设计手法。
一、文件定位:一个被 include_str! 编译进二进制的系统提示词
该提示词并非独立配置文件,而是 opencode harness 模块的静态资源,通过 Rust 的 include_str! 宏在编译期内联为字符串常量:
// codex-rs/core/src/harness/opencode.rs
const OPENCODE_TITLE_SYSTEM_PROMPT: &str = include_str!("opencode_title_prompt.md");
对应源码见 opencode.rs。同文件还声明了该 harness 的另两个提示资源——搜索子代理提示词 opencode_search_agent_prompt.md 与主系统提示词前缀 opencode_system_prompt.md(opencode.rs),三者共同构成 opencode 适配层的全部提示词资产。这些模块统一注册在 harness/mod.rs 中,与 claude_code、kimi_cli、deepseek_tui 等其它 harness 并列。
从模块组织看,harness 层的职责是“把 Codex 核心内部的消息/工具格式,翻译成某个具体上游(opencode 风格的 Chat Completions)协议可消费的形式”。标题生成是这条翻译链上的一个特例:它不经过主请求的工具装配逻辑,而是作为一次独立的、无工具的流式请求先行发出。
二、提示词全文:任务、规则与示例三块结构
提示词全文共三块:<task> 定义输出契约,<rules> 给出 14 条硬性规则,<examples> 提供 10 组少样本示例。全文如下(与仓库文件逐字一致):
You are a title generator. You output ONLY a thread title. Nothing else.
<task>
Generate a brief title that would help the user find this conversation later.
Follow all rules in <rules>
Use the <examples> so you know what a good title looks like.
Your output must be:
- A single line
- ≤50 characters
- No explanations
</task>
<rules>
- you MUST use the same language as the user message you are summarizing
- Title must be grammatically correct and read naturally - no word salad
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
- Focus on the main topic or question the user needs to retrieve
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to questions, just generate a title for the conversation
- The title should NEVER include "summarizing" or "generating" when generating a title
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
- Always output something meaningful, even if the input is minimal.
- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
→ create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
</rules>
<examples>
"debug 500 errors in production" → Debugging production 500 errors
"refactor user service" → Refactoring user service
"why is app.js failing" → app.js failure investigation
"implement rate limiting" → Rate limiting implementation
"how do I connect postgres to my API" → Postgres API connection
"best practices for React hooks" → React hooks best practices
"@src/auth.ts can you add refresh token support" → Auth refresh token support
"@utils/parser.ts this is broken" → Parser bug fix
"look at @config.json" → Config review
"@App.tsx add dark mode toggle" → Dark mode toggle in App
</examples>
2.1 <task> 块:把输出契约收敛到可机器校验的程度
任务定义的核心是“帮助用户日后检索到这次对话”(help the user find this conversation later),并给出三条可直接校验的输出约束:
| 约束 | 含义 |
|---|---|
| A single line | 标题必须是单行,避免换行污染 UI 标题栏与列表渲染 |
| ≤50 characters | 长度上限 50 字符,保证在会话列表侧栏中完整可见 |
| No explanations | 只输出标题本身,禁止任何解释性文字 |
首行 You are a title generator. You output ONLY a thread title. Nothing else. 是对模型角色与输出通道的双重限定——这类提示词属于“纯输出型子任务”,模型不需要理解对话内容去回答任何问题,只做一次抽取式概括。
2.2 <rules> 块:14 条规则的四种设计意图
规则按设计意图可分为四类:
-
语言与表达质量
- “必须与用户消息使用同一种语言”——用户用中文提问,标题就应是中文,避免检索时的语言错位;
- “语法正确、读起来自然,不要词堆砌(no word salad)”——这是对摘要类模型典型退化(把关键词直接罗列)的针对性抑制;
- “去掉 the / this / my / a / an”——标题风格上去掉英文冠词与指代词,使标题更接近标签(tag)而非句子。
-
内容聚焦与检索价值
- “聚焦用户日后需要检索到的主要主题或问题”;
- “当消息提到文件时,聚焦用户想对该文件做什么,而不仅仅是提到了它”——例如
@src/auth.ts can you add refresh token support的标题是Auth refresh token support而非src/auth.ts; - “保留精确信息:技术术语、数字、文件名、HTTP 状态码”——
500这类数字是检索锚点,必须原样保留; - “不要假设技术栈”——模型不得从零星线索推断并写入未提及的框架名。
-
防止子任务越权
- “绝不使用工具”(Never use tools)、“绝不回答用户问题,只生成标题”(NEVER respond to questions)——标题请求虽然与主请求共用同一个模型端点,但明确禁止模型把它当成正常对话轮次;
- “不得声称自己无法生成标题,不得抱怨输入”(DO NOT SAY YOU CANNOT GENERATE...)与“即使输入极简也必须输出有意义的东西”——这两条共同兜底了输入为 "hello"、"lol" 这类极简消息时的行为,配套规则要求生成
Greeting、Quick check-in、Light chat等反映语气/意图的标题。
-
抑制重复与套话
- “变换措辞,避免总是以 Analyzing 开头这类重复模式”;
- “标题中绝不允许出现 summarizing / generating”——防止模型把自我行为写进标题。
2.3 <examples> 块:10 组少样本覆盖的典型形态
示例覆盖了标题生成的几类代表性输入,每条都给出了期望输出形态:
| 输入消息 | 期望标题 | 覆盖形态 |
|---|---|---|
| debug 500 errors in production | Debugging production 500 errors | 保留数字(500)、去冠词 |
| refactor user service | Refactoring user service | 动词短语规范化 |
| why is app.js failing | app.js failure investigation | 保留文件名 |
| implement rate limiting | Rate limiting implementation | 名词短语化 |
| how do I connect postgres to my API | Postgres API connection | 问句转标签 |
| best practices for React hooks | React hooks best practices | 保留技术术语 |
| @src/auth.ts can you add refresh token support | Auth refresh token support | 文件提及 → 聚焦“要做什么” |
| @utils/parser.ts this is broken | Parser bug fix | 模糊指令 → 概括意图 |
| look at @config.json | Config review | 极简指令 |
| @App.tsx add dark mode toggle | Dark mode toggle in App | 保留组件名作为限定 |
从示例分布可以看出提示词作者刻意同时喂了“长指令”“问句”“文件引用”“模糊指令”四种输入形态,让模型对 @file 引用类消息(opencode 工作区里的常见输入)有一致的处理方式。
三、源码调用链:提示词如何变成一次真实的流式请求
3.1 请求装配:build_title_request
标题请求在 opencode.rs 的 build_title_request 中装配,最终产出的 JSON 请求体结构为:
json!({
"model": model_info.slug,
"max_tokens": OPENCODE_MAX_TOKENS, // 常量 32_000,与主请求共用
"temperature": 1,
"stream": true,
"stream_options": { "include_usage": true },
"messages": [
{ "role": "system", "content": OPENCODE_TITLE_SYSTEM_PROMPT },
{ "role": "user", "content": "Generate a title for this conversation:\n" },
{ "role": "user", "content": quote_prompt_for_opencode(&user_prompt) }
],
})
三个值得注意的实现细节:
max_tokens复用主请求上限。OPENCODE_MAX_TOKENS: u32 = 32_000(opencode.rs)同时用于标题请求与主请求(build_request,opencode.rs)。标题输出虽被提示词约束在 50 字符内,但请求参数上并未单独收紧,属于“契约靠提示词、参数靠复用”的取舍。- 用户输入只取第一条真实用户消息。
first_user_text(opencode.rs)遍历prompt.input,跳过 contextual 内容(is_contextual_user_message_content判定),返回第一条role == "user"消息的文本;取不到时回退为空字符串。也就是说标题只反映对话的第一条用户消息,而非整段历史。 - 用户消息先做 JSON 字符串引号化。
quote_prompt_for_opencode(opencode.rs)用serde_json::to_string把原文包成带引号的 JSON 字符串,再经normalize_prompt_newlines(opencode.rs)把\\n还原为真实换行(并针对printf 'SHELL_OK\n'、\nfor、\n、\nPY等常见工具输出模式做了特例修复)。这一步防止模型把命令输出里被转义的\n误读为字面量,也让多行输入以单行 JSON 字符串形式安全地进入消息体。
3.2 触发时机:should_generate_title 的两重门
并非每一轮都生成标题。should_generate_title 要求同时满足两个条件:
pub(crate) fn should_generate_title(prompt: &Prompt) -> bool {
let initial_turn = prompt.get_formatted_input().iter().all(|item| {
matches!(item, ResponseItem::Message { role, .. } if role == "user" || role == "developer")
});
initial_turn && !OPENCODE_TITLE_SENT.swap(true, Ordering::SeqCst)
}
- 首轮判定:本轮格式化输入中的所有消息都必须是
user/developer角色——即对话尚未产生 assistant 回复,这是“新会话第一条消息”的近似判据; - 进程级一次性开关:
static OPENCODE_TITLE_SENT: AtomicBool(opencode.rs)用swap(true, Ordering::SeqCst)原子地置位,保证整个进程生命周期内只触发一次标题生成,避免重连、重试或同进程多会话场景下重复请求。
3.3 harness 集成层:title_request 作为可选侧车
harness 与模型客户端之间的唯一集成点在 harness/request.rs,其头部注释明确了这一职责边界(“Adding a chat harness means adding a route arm in this module, not editing the client”)。其中 ChatHarnessRequest 结构体专门带有一个可选字段:
/// Some harnesses fire a separate title-generation request first.
pub(crate) title_request: Option<Value>,
在路由分发中,只有 ChatHarnessRoute::OpenCode 分支会条件性地填充该字段(request.rs):
ChatHarnessRoute::OpenCode => {
let title_request = should_generate_opencode_title(&guided_prompt)
.then(|| build_opencode_title_request(&guided_prompt, model_info));
let (request_body, tool_kinds) = build_opencode_request(&guided_prompt, model_info)?;
(request_body, tool_kinds, title_request, ChatHarnessPostprocess::None)
}
即:先经 harness guidance 注入(prompt_with_harness_guidance)后的 guided_prompt 同时喂给标题与主请求两条构造路径,保证两者看到的环境上下文一致。其它 harness 分支(DeepSeekTui、KimiCli、MiniSweAgent、Terminus2 等)均返回 None;claude_code 走的是客户端侧另一套带 profile 的标题逻辑(build_title_request_for_profile,见 client.rs),不经过此处。
3.4 客户端侧:标题请求先行,主请求随后
client.rs 的流式主流程中(client.rs),解构出 ChatHarnessRequest 后:
- 若
title_request为Some,先用ChatCompletionsCompatClient::stream_chat_request_value发起标题流式请求(工具集为空ToolKinds::new()),并完整消费标题流(循环title_stream.next()仅做错误映射); - 标题请求若返回 HTTP 401,走与主请求一致的
PendingUnauthorizedRetry认证恢复流程(handle_unauthorized后continue重试); - 标题请求结束后才发送真正的
request_body主请求,并对返回流应用 harness 后处理(opencode 分支为None)。
从这条链路可以推断出整体时序:opencode 会话的首轮中,模型端点会先收到一次纯文本、无工具的“生成标题”流式请求,随后才是携带完整系统提示词与工具列表的主请求;标题结果通过流式事件进入会话层,供会话列表展示与日后检索。
四、设计要点小结
结合提示词本体与调用链,这套标题生成机制有三个可直接借鉴的工程点:
- 提示词即契约,代码即执行器。 输出约束(单行、≤50 字符、无解释)全部写在提示词里,而“何时触发、取哪段输入、如何编码、如何重试”全部由
should_generate_title/first_user_text/quote_prompt_for_opencode/ 401 重试逻辑承担,提示词不掺入任何运行时状态。 - 输入面最小化。 只喂第一条真实用户消息并做 JSON 引号化,既降低误读转义的风险,也把标题语义锚定在“用户最初想做什么”上——这与
<task>中“帮助日后检索”的目标一致。 - 一次性 + 首轮的触发门。
AtomicBool与“输入全为 user/developer”的组合判据,使标题成本被限制为每进程一次,且只发生在新会话首轮,对多轮长对话的 token 开销几乎为零。
相关源码入口汇总:提示词本体 opencode_title_prompt.md、请求构造与触发判定 opencode.rs、harness 集成路由 request.rs、客户端先行发送逻辑 client.rs。修改提示词内容后,由于是编译期 include_str! 内联,需重新构建 codex-rs 才能生效。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00