通过自制插件修改 OpenCode 系统提示词
OpenCode 暂不支持原生替换系统提示词,可通过 Plugin 利用hook修改实现。
背景
GitHub Issue #7101 提出此功能几个月,不过还没有合并相关功能。我们基于Issue内的社区方案改进,确保向后兼容。
根据 Issue #7101,修改系统提示词的核心原因:
- 低上下文模型 - Phi4、gpt-oss 等本地/小型模型,上下文窗口有限(如 128K),超出后会报 context window exceeded
- 节省 token - AGENTS.md 是追加而非替换,无法精简整个提示词
- 快速迭代 - 不需要重新编译 OpenCode,可随时调整提示词
- 系统提示词过大且指令固化 - “extremely large, overly opinionated”
- 特定模型需要特定指令 - 自定义模型需要特定格式化说明
- 定制化需求 - 用户希望有更多控制权
- 与系统提示词"斗争" - “fighting the system prompts time and time again”
前言
你会因为什么原因想要修改系统提示词呢?就我个人而言,主要是第4、6、7条,首先我不觉得自带的系统提示词写得很好,而且大小10kb,性价比比较低,其次,我准备参照iflow的写法来优化一个针对不那么强的模型,因为glm现在因为用户太多了,体验不佳。为什么要用iflow的提示词呢?因为这种写法对国产模型相对友好,我觉得Claude模型很大程度是因为Claude Code这个工具设计得好,所以我也会参考Claude Code来写提示词。
基本上,我希望尽可能地在opencode上还原iflow的那种心流体验,在掌控系统提示词后,还可以开发更多适配国产模型的扩展,或者使用已有的、有用的插件或者配置项目。
说真的,这是我第一个能改系统提示词的工具,我在用那些国产IDE的时候就有一种深深的无力感,在上下文腐败之前就已经抓不住重点,总是急着想要用方案和代码来稳稳接住你
系统提示词在这里,我个人建议使用英文原版的提示词,更加精确和还原体验。
想了好久,只能把iFlow的焚诀炼化出来用一种新的形式陪伴了
友情提示:即便你是自备模型,也不要用opencode处理敏感数据,因为win上编译的版本是exe文件,而不是js文件,你看不到源码,这也是为什么我无法在本地直接从源码改系统提示词的原因。
安装
1. 创建配置文件
手动创建 %userprofile%/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["./plugins/slim-prompt.ts"]
}
2. 放置插件
复制 slim-prompt.ts 到 %userprofile%/.config/opencode/plugins/
3. 放置提示词
在提示词目录放入 default.txt
- 提示词目录优先级
- 环境变量自定义文件夹: Windows PowerShell命令
setx OPENCODE_PROMPTS_DIR ".\prompt",(设置后重启终端生效) - 或者使用默认文件夹(需手动创建):
%userprofile%/.config/opencode/prompt
4. 重启生效
原理解析
hook 在发送消息前触发,通过 "\nYou are powered by the model named " 定位分割点,保留模型信息及之后内容,只替换基础提示词。
代码
slim-prompt.ts
/**
* Opencode系统提示词精简插件
*
* 基于 @audriussagadinas 和 @distbit0 的方案改进
* 原版 Issue: https://github.com/anomalyco/opencode/issues/7101
*
* 功能:
* - 根据模型 ID 选择对应的提示词文件(与官方命名一致)
* - 使用模型信息行作为分隔符,更精确地定位替换位置
* - 只替换基础提示词部分,保留模型信息及之后的所有内容
*
* 提示词文件目录优先级:
* 1. 环境变量 OPENCODE_PROMPTS_DIR
* 2. 默认: ~/.config/opencode/prompt
*
* 使用方法:
* 1. 在 .opencode/plugins/ 目录放置此文件
* 2. 在提示词目录放置模型对应文件,默认使用default.txt,有手动指定模型的需求请自行修改promptFilename函数
* 3. 在 opencode.json 添加 "plugin": ["./plugins/slim-prompt"]
*/
import { readFileSync } from "node:fs"
import { join } from "path"
import { homedir } from "os"
/** @typedef {import("@opencode-ai/plugin").Plugin} Plugin */
// 提示词目录: 环境变量优先,否则默认用户目录
const PROMPTS_DIR = process.env.OPENCODE_PROMPTS_DIR || join(homedir(), ".config", "opencode", "prompt")
// 根据模型 ID 选择对应的提示词文件
function promptFilename(model) {
const modelId = String(model?.api?.id ?? "").toLowerCase()
// 因为我不用这些模型,所以默认使用 "default.txt"
// if (modelId.includes("claude") || modelId.includes("anthropic")) return join(PROMPTS_DIR, "anthropic.txt")
// if (modelId.includes("codex")) return join(PROMPTS_DIR, "codex.txt")
// if (modelId.includes("gpt-4") || modelId.includes("o1") || modelId.includes("o3")) return join(PROMPTS_DIR, "beast.txt")
// if (modelId.includes("gemini")) return join(PROMPTS_DIR, "gemini.txt")
// if (modelId.includes("trinity")) return join(PROMPTS_DIR, "trinity.txt")
// if (modelId.includes("kimi")) return join(PROMPTS_DIR, "kimi.txt")
return join(PROMPTS_DIR, "default.txt")
}
// 检查字符串是否为基础提示词(OpenCode 默认开头)
function isBasePrompt(system) {
return system.startsWith("You are OpenCode") || system.startsWith("You are opencode")
}
function replaceBasePrompt(system, replacement) {
// 找到模型信息行的起始位置
const boundary = system.indexOf("\nYou are powered by the model named ")
if (boundary === -1) return null
// 检查分割点前是否是基础提示词
const prefix = system.slice(0, boundary)
if (!isBasePrompt(prefix)) return null
// 替换: 新提示词 + 原始内容中分割点之后的所有内容
// 只替换基础提示词部分,保留模型信息、环境信息、skills、AGENTS.md
return replacement + system.slice(boundary)
}
/** @type {Plugin} */
export const SlimPromptPlugin = async () => ({
"experimental.chat.system.transform": async (input, output) => {
const [system] = output.system
if (!system) return
// 根据模型选择对应的提示词文件
const shortPrompt = readFileSync(
promptFilename(input?.model),
"utf8",
).trim()
// 执行保守替换
const next = replaceBasePrompt(system, shortPrompt)
if (!next) return
// 更新系统提示词
output.system.splice(0, output.system.length, next)
},
})
export default SlimPromptPlugin
补充:调试钩子
如需查看完整系统提示词结构,可安装 hook-debug.ts:
{ "plugin": ["./plugins/slim-prompt.ts", "./plugins/hook-debug.ts"] }
输出保存在 ~/.config/opencode/plugins/debug/ 目录。
hook-debug.ts
/**
* Hook 调试插件 - 用于捕获系统提示词的完整上下文
*
* 用途: 捕获 experimental.chat.system.transform hook 的输入输出
* 帮助理解 OpenCode 系统提示词的结构
*
* 输出: 插件同目录下的 JSON 文件
*/
import type { Plugin } from "@opencode-ai/plugin"
import { existsSync, mkdirSync, writeFileSync } from "fs"
import { join } from "path"
export const HookDebugPlugin: Plugin = async () => ({
"experimental.chat.system.transform": async (input, output) => {
// 构建调试对象
const debug: any = {
timestamp: new Date().toISOString(),
sessionId: input?.sessionID ?? "unknown",
model: input?.model ?? null,
systemLength: output.system?.[0]?.length ?? 0,
// 完整的原始系统提示词
systemRaw: output.system?.[0] ?? "",
}
try {
// 生成文件名: hook-debug-ses_xxx-2026-04-18T12-00-00.123.json
const filename = `hook-debug-${debug.sessionId}-${debug.timestamp.replace(/[:.]/g, "-")}.json`
// 在写入前检查并创建 debug 文件夹
const debugDir = join(__dirname, "debug")
if (!existsSync(debugDir)) {
mkdirSync(debugDir, { recursive: true })
}
// 输出文件放到 debug/ 子文件夹
const filepath = join(__dirname, "debug", filename)
writeFileSync(filepath, JSON.stringify(debug, null, 2))
// console.log生成的信息可能会覆盖TUI界面
// console.log(`[HookDebug] Saved: ${filename}`)
} catch (err: any) {
// 写入失败时,将错误信息也写入调试对象
debug.error = err.message ?? String(err)
debug.errorTime = new Date().toISOString()
// 尝试写入错误日志文件
try {
const errorFilename = `hook-debug-error-${Date.now()}.json`
writeFileSync(join(__dirname, "debug", errorFilename), JSON.stringify(debug, null, 2))
} catch {}
console.error("[HookDebug] Save failed:", err.message)
}
},
})