dsh-huoren
给 DSH 的模型输出加活人感的插件。
活人感不是「加语气词」。活人感是把一个人用手机打字的物理约束加回去: 屏幕小所以话短,手慢所以一条一条发,脑子会走神所以有停顿,会手滑所以打错字, 有情绪所以前后不一致。
灵感来自 MaiBot(麦麦,开源 QQ 机器人)的工程实现: 它的设计原则是一句话——「最像而不是好」。
与 dsh-doubao 的关系:那个加了「人机感的壳」,这个反过来拆壳。 两者语料不重叠,机制也不重叠。
两条通道
| 通道 | 机制 | 管什么 |
|---|---|---|
| ① 系统提示注入 | systemPrompt.section({ order: 166 }) |
「说什么」——口气、长度、立场、禁止句式 |
| ② 输出流后处理 | ctx.on('llm/stream', …) |
「怎么发」——分条、节奏、毛边,模型管不到的 |
可以单独关。只留 ① 更像真人但节奏仍是机器;只留 ② 节奏对了但用词还是客服腔。
五个「活人感来源」
这不是五个功能,是五条互相独立的物理事实。缺一条就像 AI。
1. 节奏:人不会一次吐一整段
pacing.js。时间模型对齐 MaiBot 的 calculate_typing_time():
每条耗时 = Σ 单字耗时 × typingSpeed × (1 ± jitter)
单字耗时:中文 charMs(默认 90ms)、英文数字 ×0.5、空白 ×0.15
标点额外:句号/问号 +pausePunctMs ×1.4,逗号 ×0.6
单个中文字 ×3(一个字要犹豫一下)
上限:maxDelayMs(默认 6s),防止把界面冻住
第一条之前是 thinkMs(默认 800ms)——人不会秒回。
三种时序模式:burst(一条一条发,默认)/ token(保留流式只加抖动)/ off。
2. 分条:人不会写小作文
split.js。抄 MaiBot 的 [response_splitter]:句末断句 → 概率性合并 → 压到 splitMax 条。
- 断句点:
。!?与,;、,但不切在引号、括号、代码块中间 - 英文小数点、数字里的逗号不切
- 编号列表、表格、引用整块独占一条,不给它加语气词
- 含代码块 / 表格 / 图片的回复整段不拆(
skipCodeBlocks)
3. 毛边:会手滑,也会自己发现
humanize.js + phrase.js。
| 项 | 做法 | 参数 |
|---|---|---|
| 句首口头语 | 「嗯」「哦对」「等下」「其实吧」「讲真」 | fillerRate |
| 句尾语气词 | 「。」→「吧」「呗」「啊」「嘛」 | particleRate |
| 错别字 | 40 组常用易混词(应该→因该、已经→以经…),一次回复最多一个 | typoRate |
| 自我更正 | 打错后补一条「打错了,是X」「(X)」 | correctionRate |
| 摆烂 | 太长或被问烦了,直接「不知道哦」 | lazyChance |
保护:代码块、行内代码、结构化块一律不碰。问句不加语气词(「好不好啊?」听着像撒娇过度)。
4. 去 AI 腔:这是反向通道
clean.js。豆包语插件加壳,这里拆壳。
删的:作为 AI 助手,、希望这对你有帮助、如果还有其他问题随时问我、总的来说、综上所述、需要注意的是、以下是、我理解你的感受。
改的:不是A,而是B → 不是A,是B;—— → ,;短句句末 。 → 去掉;emoji → 删。
硬保证:删完如果是空的,退回原文。绝不吞掉模型说的话。
5. 情绪心流:有状态才叫活人
mood.js。两个值,按会话落盘(~/.dsh/dsh-huoren/mood-<session>.json):
energy精力:被催 −3、深夜 −4、干活(工具轮次)−Nwarmth热度:被谢 +3、被骂 −8、被分享私事 +3
每轮向基线(60/55)回落 6%,所以不会永久卡在一个状态。 状态最后变成提示词里的一句话:「你现在有点累,回话短,别铺垫」。
档位
| 参数 | 关闭 | 轻度 | 自然(默认) | 浓烈 |
|---|---|---|---|---|
| 最多几条 | 0 | 2 | 3 | 5 |
| 每条字数 | 60 | 80 | 45 | 30 |
| 打字速度 | 1 | 1.2 | 1 | 0.8 |
| 首条思考 ms | 0 | 450 | 800 | 1100 |
| 标点停顿 ms | 0 | 140 | 240 | 320 |
| 抖动 % | 0 | 30 | 45 | 60 |
| 口头语 % | 0 | 12 | 25 | 40 |
| 语气词 % | 0 | 20 | 40 | 60 |
| 错别字 % | 0 | 10 | 25 | 40 |
| 自我更正 % | 0 | 40 | 60 | 70 |
| 摆烂 % | 0 | 0 | 2 | 8 |
| 情绪强度 | 0 | 30 | 55 | 75 |
「关闭」档强制把所有参数清零,残留旧值不会漏进来。
实测
输入(模型原文):
作为 AI 助手,我先说明一下。这个 bug 已经修好了,改的是配置文件第三行的缩进。
总的来说不复杂,希望这对你有帮助!
输出(自然档):
[0] 我先说明一下。这个 bug 已经修好了,
[1] 其实吧,改的是配置文件第三行的缩进。不复杂,呢
时间戳(burst,charMs=60,thinkMs=300):
302ms → 日志我看过了,问题出在配…
1653ms → 第三行的缩进错了,YAM…
2605ms → 我改成两个空格就好了。
设置页
设置 → 活人感。所有旋钮实时预览(预览会按 seed 复现同一结果),保存即重载,不用重启 DSH。
预览区直接渲染成气泡,能看到「会拆成几条、每条长什么样、有没有补一条更正」。
与模型无关的部分
这套改造不依赖模型配合。任何模型吐出来的正文都会经过同一套管线, 所以它同样作用于:别的 provider、压缩摘要以外的轮次、以及你自己写的插件产出的文字。
不处理的轮次:session-title、compaction、auto-review、expression(skipPurposes),
以及以工具调用为主的轮次(skipToolTurns)。
开发
npm test # 33 个测试,不依赖宿主
lib/
config.js 档位 / 参数 / 归一化
phrase.js 词库:口头语、语气词、AI 腔黑名单、易混词表
clean.js 去人机化(代码块 / 列表 / 表格保护)
split.js 断句 / 合并 / 压条数
humanize.js 口头语、语气词、错别字、自我更正
pacing.js 打字时间、思考时间、可中断 sleep
mood.js 情绪状态机
prompt.js 系统提示段落
render.js 管线编排(清理 → 分条 → 毛边)
transform.js llm/stream 瀑布:拆块、重排 index、分条发送
routes.js 设置页同源接口
index.js 宿主入口
client.js 设置页 UI(web 平台)
踩过的坑
yield*会静默丢弃return值。直通函数如果return [chunk], 整条工具流会凭空消失。必须yield。- 必须重排 block index。把一个文本块拆成三条后会占掉 0/1/2, 上游后面的工具块还是 1 号,直接撞号。要维护「上游 index → 我的 index」映射。
- 改了
text-delta就必须改block-end.block.text。装配器以 block 为准, 只改增量会被覆盖。 - 删套话要只删前缀。
作为AI助手,后面那半句是人话, 一整句吃掉就等于吞掉内容。
已知取舍
burst模式会牺牲 token 级流式:一段文字会等打完才出现。这是刻意的——人就是这样发的。 想要流式就切token模式。- 错别字只在最后一条出现,且一次回复最多一个。多了就是坏掉了,不是人。
- 情绪状态在提示词通道里读的是
default会话的状态;输出流通道按sessionId分开存。 - 不做「假装不是 AI」。用户直接问「你是不是 AI」,照实说。活人感是质感,不是欺骗。
License
MIT
No comments yet. Be the first to write one.