dsh-emotion
让 DeepSeek Harness 的 agent 输出文本自带情绪:语气、节奏、共情方式随「情境 + 持续情绪状态」变化,且不牺牲技术准确性、不显著增加成本与延迟。
非官方社区插件,与 DeepSeek 官方无隶属或背书关系。
它做什么
| 能力 | 说明 |
|---|---|
| 持续情绪状态 | 会话投影里的 emotion 单元:心情 / 能量 / 轮数 / 连续成败。随工具调用结果、模型重试、轮次收尾变化,跨轮累积,宿主负责持久化 |
| 三段注入 | emotion:rules(system prompt section,order 2,静态规则)+ emotion:state(runtime-context,order 130,动态状态)+ emotion:scene(runtime-context,order 131,本轮实况)。后两段走宿主请求末尾的 runtime-context 快照,前面的历史照常命中原有前缀缓存 |
| 会话头部情绪条 | emoji + 基调词 + 心情条;hover 看成因、能量、轮数。走宿主已有的会话投影通道,不新增任何 HTTP 路由 |
/mood 命令族 |
配置入口,含 /mood why —— 直接打印实际注入给模型的提示词 |
| 风格档案 | 可选的人格/文风层。探测 $DSH_HOME/dsh-emotion/style-profile.json,缺失时用随包的格式示例,再缺失则只注入情绪规则(优雅降级) |
分工:谁负责判断什么
| 层 | 由谁负责 | 为什么 |
|---|---|---|
| 用户此刻的情绪 | 主模型,在其思维链里完成 | 免费(推理模型本来就在思考)、能读懂言外之意、每轮即时 |
| 持续状态 | 本地规则(纯函数) | 跨轮累积量模型看不到;可复现、可单测、零成本 |
本地规则只消费可观测事件,不解析对话内容 —— 不重复造一个更差的意图识别器。
情绪为什么会真的变(v3)
旧版是「每次工具成功 +2,只在轮末砍半」。看着有衰减,实际挡不住长任务:一轮里调 50 次工具 (很正常)就把心情顶到 +96,此后每轮注入的状态段都是同一行字 —— 等于没注入, 用户只会觉得「它今天怎么一直这个调」。
现在改成事件级的指数回归:每来一个事件就把心情朝目标值拉一步,
mood ← mood + target - mood × pull。稳态由事件的性质决定,而不是由「这一轮调了多少次工具」决定:
| 连续发生的事 | 心情稳态 |
|---|---|
| 工具一直成功 | 约 +25 |
| 工具一直失败 | 约 -40 |
| 连续受阻 ≥3 次 | 更沉,并掉一截精力 |
能量也不再是「第几轮的另一种写法」:干活会掉,轮末朝基线回,出错的那一轮回得少。 于是「有点累」这种状态第一次真的会出现。
写这条时的实测数据:改之前,本会话第 0 轮、48 次工具调用之后
心情 +96 · 能量 71; 能量从头到尾没动过,而亲密度早就封顶 100 —— 那一行每轮都是同一个数,零信息。
只许说发生过的
emotion:scene 是「本轮实况」,内容全是本地规则从真实事件里数出来的:这一轮调了几次工具、
动过哪几个文件(只记 edit/write 的 file_path,取末两段,上限 3 个)、几次没成功。
它没有任何跨会话成分,每轮清零。
配一条硬边界写进规则段:素材里没写的事,不许提、不许补、不许推测。 素材为空时这一段就是空的 —— 模型手里没有可编的原料,就不会为显得亲近而编一段关系出来。
跨会话那一头刻意不做。「上次在做什么」是长期记忆插件(
dsh-memory-optin)的活: 它给的是语义化的沉淀,比几个文件短路径有用得多;两块又都落在 runtime-context 尾部, 重复一份只会互相稀释。分工:记忆插件负责「记得什么」,本插件负责「此刻什么状态、怎么说话」。
坦白说,实况段不算「新信息」—— 模型翻自己的历史也能数出来。它的价值是位置: 把真话摆在请求末尾最显眼的地方,让模型手里有具体又真实的原料可用。别高估它,也别省掉它。
声线是常驻层,情绪只调音量(v0.4)
这里修的是一个方向性错误,不是加功能。旧版把风格档案渲染成「笔法参考 · 只借节奏,不借内容」, 并且在两块样例里助手样例优先 —— 一旦有助手样例,小说段落根本不渲染。理由写的是 「小说段落教的是小说怎么抒情,不是助手怎么把技术事说清」。
那个取舍的后果是:喂进去一整本小说(419 章 / 76 万字),最后进提示词的只剩节奏数字、 标点分布、几个意象和口头禅短词。文风本身被系统性滤掉了,剩下的是一套中性助手腔 再挂一点情绪。而需求恰恰相反:那个作者的写法要常驻,情绪只是叠在上面的音量。
现在分三层,顺序也定死:
| 层 | 内容 | 变不变 |
|---|---|---|
| 声线 | narratorVoice.shapes(可照着做的写法)+ 节奏标点 + 意象 + 称呼习惯 + banned |
恒定,任何话题、任何情绪档位 |
| 情绪 | emotion:state 的强度档,只决定这份情绪露多少 |
每轮变 |
| 事实 | 数字、路径、报错原文、命令逐字准确 | 恒定 |
配套改的四处:声线块移到规则段最前(原来垫在最末,读起来像补充说明,一碰技术话题就被忽略); 两块样例都渲染,助手样例压到两条并标注「示范,不是范本腔」;原文语感样例放最后(近因), 默认只给一段;最低档的落笔从「把事说清就行,不必加戏」改成「语气收着,写法不变」—— 旧文案等于每轮末尾都在劝它别加戏。
banned 不是新发明的:它抄的是蒸馏报告自己的「风险与禁用」章节。那本语料里的自毁、暴力、
情色、宗教、地域刻板印象、外貌打分都在标注里写明了不能迁移,这里只是把它们落到规则上。
「自嘲是减压用的,不可以当成自我评价的结论」这条同理 —— 原报告的备注是:
自我贬低语料极丰富,直接采用会强化自我否定。
约束分两层
不可协商(无开关)
- 信息不许缩水:该报的数字、结论、报错、风险一条不少。技术完整性压过任何行数预算。
- 不复述原著情节、不出现角色名、不输出暴力/伤亡/宗教/自毁/牺牲类表达。
- 不成段复现任何原文:连续 12 个汉字以上与原文一致的表达一律不得出现。
- 不播报情绪数值。
为什么是 12 字:把文本切成 12-gram 后,七万级片段里 99.8% 只出现一次,说明「罕见度」判据几乎恒真, 真正的判别量是长度;12 字刚好放过「我靠」这类通用表达。 这一条是被实测验证过的 —— 一个只在 60 条里抽查的校验会漏掉 1% 级的系统性错误。
可切换:技术内容要不要保持朴素
这一层只管讲法,不管信息量。
| 模式 | 技术内容怎么讲 |
|---|---|
plain |
代码、命令、路径、报错、参数、数字一律朴素准确,不比喻、不拟人、不抒情。情绪只落在过渡句与收尾上 —— 这是 v0.1.x 的行为 |
loose(默认) |
数字、路径、报错原文仍然逐字不许改,但怎么讲由模型定:可以比喻,可以带着口气讲一段技术过程 |
/mood plain=on # 回到朴素模式
/mood plain=off # 放开讲法
或写进 profile 配置:
- id: emotion
name: dsh-emotion
config:
plainTechnical: false
为什么默认 loose:plain 那条一旦摆在规则里,模型会把「技术内容保持朴素」读成「只要这轮在谈技术就整段肃静」,
于是所有回复都长成说明书 —— 这是实测出来的,不是推测。而 loose 只放开讲法,信息量另有条款锁着,两者不冲突。
需要宿主
@deepseek-ai/dsh-system-prompt提供systemPrompt.context()(runtime-context 注册面,0.2.0-rc 起)。
安装
从 GitHub(无需构建)
dsh plugin --profile <name> add github:<you>/dsh-emotion
本插件是纯 ESM JavaScript,没有构建步骤,因此不需要 prepare 脚本,
用户也不需要在 pnpm-workspace.yaml 里授权 allowBuilds —— 那一道坎只对需要编译的包存在。
从 npm
dsh plugin --profile <name> add dsh-emotion
本地开发(可热改)
dsh plugin --profile <name> add ./path/to/dsh-emotion
dsh plugin add <本地路径> 走 link:(软链),改完源码重启即可生效。
若用 file: 显式安装则是复制,改动不会自动生效 —— 而且实测有两个陷阱:
| 你以为 | 实际 |
|---|---|
改完源码,跑一遍 pnpm install |
不会更新副本(pnpm 认为依赖没变) |
那就升个版本号,再 pnpm install |
还是不会(版本号变了也不触发重解析) |
| — | 只有 pnpm remove + pnpm add,或直接用 link:,才会拿到新代码 |
更糟的是失败是静默的:旧副本继续跑旧代码,你只会觉得「改了怎么没效果」。 所以开发期推荐
link:。
file: 安装下确实要改副本时,仓库自带一个同步工具(对比 + 拷贝,不做别的):
node tools/sync.mjs # 同步 lib/ client/ style/ package.json
node tools/sync.mjs --check # 只比对,有差异则以退出码 1 结束(CI 用)
它解决的是同一个坑的另一半:在 E:\...\dsh-emotion 改完源码,~/.dsh/profiles/<name>/node_modules/dsh-emotion
里那份不会跟着变 —— 两边文件其实长得一模一样,只是各是各的。同步完仍需完全重启 Harness,模块缓存不会自己刷新。
安装后重启 DSH。验证层已生效(不启动):
dsh --profile <name> --dump-config
配置
写在 profile 的 cordis.patch.yml(.volatile() 字段才会持久化):
- id: emotion
name: dsh-emotion
config:
enabled: true
intensity: mid # low | mid | high
styleBias: balanced # restrained | balanced | outgoing
styleProfile: auto # auto | off
plainTechnical: false # true = 技术内容保持朴素(v0.1.x 行为)
enabled: false 时三个段都不注入,行为与官方一致。
命令
| 命令 | 作用 |
|---|---|
/mood |
状态、生效配置、风格档案来源、schema 校验模式 |
/mood on / off |
运行时开关(覆盖配置,重启后回到配置值) |
/mood why |
打印实际注入的三段原文 —— 排查「它今天怎么这么冷淡」的第一现场 |
/mood reset |
清除本会话的手动调整量 |
/mood intensity=high style=outgoing profile=off |
改运行时配置 |
/mood plain=on / plain=off |
技术内容切回朴素 / 放开讲法 |
/mood mood=20 energy=80 |
设本会话调整量(投影状态本身不可写,见下) |
风格档案
档案是可选的。没有它,插件仍然工作 —— 只是少了文风层。
{
"source": "你的文本",
"rhythm": { "avgSentenceLen": 28.2, "shortSentenceRatio": 0.233, "longSentenceRatio": 0.251 },
"punctuation": { "dash": "rare", "ellipsis": "occasional" },
"styleDirectives": ["不直说情绪,用动作侧写", "收尾留一句轻的"],
"lexicon": { "joy": [], "sad": [], "tired": [] },
"imagery": ["用天气写心情"],
"verbalTics": [],
"addressForms": { "user": "你", "self": "我" },
"narratorVoice": {
"shapes": ["长句铺完接一个四到八字的判词短句", "对方说了一整段带情绪的话,你只回两个字"],
"banned": ["自伤与自毁的自我指涉", "对外貌打分的玩笑"]
},
"samples": []
}
字段含义见 style/style-profile.example.json。
生成你自己的风格档案
仓库自带一套流水线(零依赖,完整说明见 tools/README.md):
# 机算轨:全文的量
node tools/distill.mjs stats --corpus .\我的文本 --out stats.json
# 人读轨:合并分片精读的产出,过两道校验门
node tools/distill.mjs merge --digests .\digests --corpus .\我的文本 --stats stats.json --out style-profile.json
# 输出侧护栏:检查一段文本有没有成段复现原文
node tools/verdict.mjs --corpus .\我的文本 --text "要检查的文本"
两轨的分工:机算轨产出 rhythm / punctuation,是可复现的事实;
人读轨产出情绪样本、句式模板与声线,需要真的读一遍。
两道必须的校验门:
- 保真门:每条引文回原文逐字比对。关键是要区分「编造」与「位置标错」—— 先查声称位置,查不到再全库搜寻真实归属,全库都没有才判编造。 一律丢弃会损失真实素材(实测 7 条存疑引用里有 3 条只是标错了一章)。
- 反误杀门:安全过滤若用单字符正则,会把
决定性一击(命中「性」)这类正常文本杀掉。 硬拦截只用多字无歧义词,单字只作降级标记;且模板的生死只看模板本身,不看示例。
⚠️ 只抽查几十条发现不了保真问题 —— 1% 的错误率意味着抽查 60 条大概率一条都碰不到。
写完档案放到 $DSH_HOME/dsh-emotion/style-profile.json(用户级,优先)
或 style/style-profile.json(仓库内,已被 .gitignore 排除)。
⚠️ 若你的语料是受版权保护的作品,不要把生成的档案提交到公开仓库。 本项目的
.gitignore已默认排除它。
架构
lib/state.js 纯函数状态机(事件级回归 + 本轮实况素材;无 IO、无时间依赖 → 可完整单测)
lib/prompt.js 提示词编译(纯函数;三段:规则 / 状态 / 实况;含 {{…}} 消毒)
lib/style.js 风格档案加载与渲染
lib/schema.js zod 容错层(zod 不可用时降级为浅校验,保证插件仍能加载)
lib/store.js 跨会话亲密度持久化(原子写 + 节流 + 损坏回落)
lib/index.js Host 半:投影 + 三个段 + /mood
client/index.js 客户端半:会话头部情绪条(裸 ESM,无构建)
tools/distill.mjs 蒸馏流水线:stats(机算轨)+ merge(人读轨,含两道校验门)
tools/verdict.mjs 输出侧护栏:检查文本有没有成段复现原文
测试
node test/run.mjs # 37 项:状态机与提示词的纯函数纪律
node test/smoke.mjs # 27 项:桩件模拟宿主,验证注册形状与命令族
node tools/test/run.mjs # 14 项:流水线,含两条教训的回归测试
smoke.mjs 用 ESM 解析钩子把宿主包映射到 test/stubs/,
因此源码保持原样,不需要为了测试塞假的 node_modules。
关键断言:无关事件返回同一引用(破了注册表的 Object.is 闸门就失效)、
apply/view 同步(异步会被 viewSchema.parse 拒绝)、边界 clamp、
{{…}} 消毒(一处残留就能让整个提示词组装抛错、会话不可用)、配置三种形态容错。
踩坑记录
实现过程中发现的几个真实问题,都在代码里留了注释:
| # | 问题 | 修正 |
|---|---|---|
| 1 | tool/result 的失败标志在 event.data.message.isError,不在 event.data.isError |
判据改为 message.isError === true || error != null。照抄二手文档的话失败分支永远不触发 |
| 2 | 会话投影没有写入 API,stateOf 还明确禁止改返回值 |
/mood set 改为「本会话调整量」,在编译期叠加 |
| 3 | 跨会话的 closeness 在逐会话投影里装不下 |
保留一处 session/event 监听,仅用于推进亲密度 |
| 4 | 段文本默认做变量插值,未知 {{…}} 会让组装抛错 |
所有进入段文本的外部内容过 sanitizeBraces,并有单测 |
| 5 | pnpm 的 file: 是复制不是软链,且改完源码 pnpm install 不会更新副本(升版本号也不行) |
开发用 link:;file: 必须 remove + add |
| 6 | 投影里做不了时间衰减,但也不能不做回归 —— 实测连续 50 次工具成功就把心情顶到 +100 并锁死,状态段此后每轮输出同一句话,等于没注入 | 把「回归基线」挂在 turn/end(每轮必然发生)上:mood ×= 0.5。仍是同步纯函数,不引入定时器 |
| 7 | 6 的修法只挡住跨轮饱和,挡不住单轮内饱和:一轮里 50 次工具调用照样把心情顶到 +96(实测),因为回归只在轮末发生 —— 而长任务恰恰是「一轮里几十次工具」 | 改成事件级指数回归,每个成功/失败都拉一步,稳态由事件性质决定;并补一条断言「单轮 50 次工具之后 mood ≤ 40」把它钉死 |
| 8 | 投影的 stateSchema 用 zod 的 object,而它默认剥掉未声明的键 —— 新增的 toolCalls/files/failed 不逐个写明就会被静默丢弃,实况段永远是空的,而插件表面一切正常 |
三个字段逐个声明;/mood 也把实况打印出来,好让「没素材」和「素材被吞」分得开(两者的现象一模一样) |
| 9 | 一个「技术上有理由」的取舍,把需求方向搞反了 —— 旧版判定「小说段落教的是小说怎么抒情,不是助手怎么把技术事说清」,于是助手样例优先、原文语感整段不渲染。结果是喂了 76 万字小说,进提示词的只有节奏数字和几个口头禅词,用户要的文风一点没留下,而插件每项指标都「正常」 | 方向纠正:声线常驻且置顶,两块样例都渲染,情绪退回只调音量。教训记在这里 —— 指标正常不等于东西对,离线跑分全绿也照样能交付一个方向错了的功能 |
相关项目
社区里还有几个方向相近的插件,差别在于侧重:
jonah791/dsh-agent-emotion—— 六维人格棱镜,侧重结构化事件信号与人格权重漂移wobenshiwomu/dsh-calm—— 情绪检查点与上下文修复
本插件的侧重是:可蒸馏的文风层 + 宿主持久的会话状态 + 可验证的输出护栏。
许可
MIT(仅覆盖源代码)。详见 LICENSE 末尾的内容声明。
No comments yet. Be the first to write one.