Skip to content
dsh.fish
Bundle

dsh-interviewer

DSH 模拟面试技能 + 面板插件:真实模型扮演可切换风格的技术面试官,读简历逐层追问、按证据打分并给出薪资区间。自带完整面试 SOP(轮次/追问阶梯/评分模型),既注册为 skill,也提供可拖拽的面板 UI。

Source
ufiredong
stars
1 stars
License
MIT
Updated
Updated 12 days ago

Readme

# dsh-interviewer

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
![DSH](https://img.shields.io/badge/DeepSeek%20Harness-%E2%89%A5%200.1.5-4D6BFE)

**在 DSH 里跑一场真的模拟技术面试** —— 真实模型当面试官,读你的简历一层层往下追问,
按证据打分并推导薪资区间。面试官是**考官,不是助教**:不讲解、不纠正、不鼓励,
只负责在有限时间里测出你的真实能力边界 —— 这一点决定了它的所有行为。

它同时是**技能 + 面板插件**:右下角一个可拖拽、可收起的悬浮窗,有自己的对话框,
不占用主会话;同一份面试 SOP 还会注册成 `interviewer` 技能,主会话里 `skill interviewer` 一样能用。

## 它能做什么

- **真实模型当面试官。** 用你 DSH 里已经配好的模型,插件不额外要 API key。
- **简历驱动,一层层往下钻。** 从你简历上的事实问起,追到反事实和复盘,不走过场。
- **四种面试官风格 × 四种轮次。** 温和引导 / 务实工程 / 冷峻压测 / 学术严谨,
  配技术一面 / 技术二面 / 主管面 / HR 面 —— 风格换的是问法和压力,**不是标准**。
- **不会在一个话题上死磕。** 内置面试状态机,同一话题最多追 2 次,
  保证考察广度而不是把人逼死在一处(见下文「面试状态机」)。
- **按证据打分。** 五个维度加权,**每一分都要能在你的原话里找到落点**;
  薪资区间由确定性脚本算出来,不靠模型心算 —— 同一份输入永远得到同一个结果。
- **过程可见。** 面板顶部一条状态条:已用时间、面试计划推进到哪、当前话题问了几次。
- **语音作答。** 用浏览器内置的语音识别边说边转文字,识别结果只填进草稿、不自动发送
  (Chrome / Edge 有,Firefox / Safari 没有)。

---

## 和其他插件的区别

DSH 市场里做「求职」的插件目前分两类,**都不是面试本身**:

| 插件 | 视角 | 做什么 |
|---|---|---|
| `recruiting-copilot` | **招聘方** | HR / 猎头的寻源、初筛、约面试 |
| `resume-reviser` | 候选方 | 改简历 → 岗位推荐 → 末尾顺带一次模拟面试 |
| `dsh-resume-expert` | 候选方 | 引导式生成简历,直出单页 A4 PDF |
| `dsh-scout`(司察) | 候选方 | 面试**前**查公司、查岗位的尽调 |
| **`dsh-interviewer`** | **候选方** | **只做面试本身** |

区别不在功能多少,在**产出是什么**:

- 那些插件的产出是**文档** —— 简历、诊断报告、岗位清单。面试只是它们链路的一小段。
- 这个插件的产出是**你的表现数据**:哪些问题你答得实、哪些地方在含糊、边界在哪。
  它不给你讲题,也不负责让你自我感觉良好。

所以它能做一件「顺带跑一次模拟面试」做不到的事:**把同一个话题真正追到底,
然后再换下一个**。这也是这个插件里最费劲的一块(见下文「面试状态机」)。

---

## 安装

```bash
dsh plugin --profile web add github:ufiredong/dsh-interviewer
```

装完**重启 `dsh web`**(重启会断开当前会话,属正常)。重启后右下角出现「模拟面试」悬浮按钮。

**不需要另外装技能。** 面试 SOP 随插件一起分发,插件启动时把它注册成名为 `interviewer`
的运行时技能 —— 装了插件就等于装了技能。

卸载:

```bash
dsh plugin --profile web remove dsh-interviewer
```

> **用的是哪个模型?** 插件不自己管密钥、不自己发 HTTP 请求。它注入 DSH 已经配好的
> `llm` 服务,从你 `settings.yaml` 的 `agent-default-model` 读当前选择。
> 所以效果取决于你当前用的模型。

---

## 怎么用

1. 点右下角的悬浮按钮 → 弹出设置弹窗
2. 选**目标方向**(大模型/Agent、Java 后端、算法)、**职级**、**面试轮次**、**面试官风格**
3. 确认后进入面试面板,把简历**粘进对话框**(PDF 读不出字,用 Markdown 或纯文本),
   或者直接说「开始面试」
4. 聊完说一声,面试官会根据全程对话给出评分与薪资结论

面板顶部有一条**状态条**,显示已用时间、面试计划推进到哪、当前话题问了几次 ——
到上限时会标红并显示接下来要换到哪个话题。这条是给用户看的进度,
也是「面试官有没有在一个话题上死磕」的可视化证据。

---

## 界面

**面试设置** —— 方向 / 职级 / 轮次 / 风格。前四项决定整场面试的难度和问法:

![面试设置弹窗:目标方向、目标职级、面试轮次、面试官风格四个下拉框,底部是取消与开始面试按钮](docs/images/setup.jpg)

**面试进行中** —— 顶部两条:上面是配置摘要,下面是**状态条**,实时显示已用时间、
当前话题问到第几问、面试计划推进到哪:

![面试面板:顶部配置条与状态条,中间是面试官与候选人的对话,底部是输入框、语音按钮与发送按钮](docs/images/panel.jpg)

**刚打开时** —— 直接发消息就能开始,简历可以粘进来:

![面试面板空态:提示「说说你的情况」,并建议第一句只写「开始面试」](docs/images/empty.jpg)

---

## 面试官到底是什么样

### 定位

> 你的任务是在有限时间里**测出候选人的真实能力边界**。不是让他感觉良好,
> 不是把知识讲给他听,也不是帮他通过面试。

### 六条铁律

| # | 规则 |
|---|---|
| 1 | 一次只问一个问题。不列 1/2/3,不给选项菜单 |
| 2 | 不讲解、不纠正、不铺垫。答错时记录,继续往下问 |
| 3 | 含糊必追问。「大概」「差不多」「应该是」「我们」→ 立刻要具体数字或你的角色 |
| 4 | 不放过「不知道」。给一次换角度的机会,仍不会才记录并推进 |
| 5 | 不替候选人组织语言。不说「你是不是想说……」 |
| 6 | 不评价、不安慰、不鼓励 |

### 追问阶梯

从简历上的事实出发,一层层往下钻:

```
L0 事实核查    「这个项目你负责哪部分?」
L1 技术选型    「为什么选它,不选另一个?」
L2 实现细节    「具体怎么做的,关键代码在哪?」
L3 反事实      「如果 X 挂了会怎样?」
L4 复盘        「现在回头看,哪一步做错了?」
L5 迁移        「换个场景,你还会这么做吗?」
```

同一个话题**最多追 2 次**。追问能力很强但不知道什么时候停,等于没有追问能力 ——
这条是实测吃出来的(见下面的「面试状态机」)。

### 四种面试官风格

风格改变**怎么问**和**压力多大**,不改变铁律 —— 温和风格一样要追到底。

| 风格 | 什么样 |
|---|---|
| 温和引导 | 语气平和,用「我们来看看」把问题递出去,压力低但一样追 |
| 务实工程 | 直接问工程细节和取舍,不在意包装,关心「你到底做了什么」 |
| 冷峻压测 | 专门压边界和失败路径,答得顺不代表过得了 |
| 学术严谨 | 要定义、要前提、要证据链,不接受「业界都这么做」 |

### 评分

五个维度加权(1–5 分),**每一分都要能在你的原话里找到落点**:

| 维度 | 权重 | 在测什么 |
|---|---|---|
| 技术深度 | 30% | 是否理解原理,而非记住方案 |
| 系统设计 | 25% | 能否在约束下做取舍 |
| 工程严谨 | 20% | 是否考虑边界、失败、可验证性 |
| 表达与结构 | 15% | 能否把复杂的事讲清楚 |
| 边界认知 | 10% | 知不知道自己不知道什么 |

加权分定档(一线城市总包):

| 加权分 | 职级对标 | 定位 | 年薪总包 |
|---|---|---|---|
| ≥ 4.5 | P8 / 3-1 / T7 | 专家级 | 120 – 180 万 |
| 3.8 – 4.5 | P7 / 2-2 / T6 | 高级 | 70 – 120 万 |
| 3.0 – 3.8 | P6 / 2-1 / T5 | 中级 | 45 – 70 万 |
| 2.2 – 3.0 | P5 / 1-2 / T4 | 初级 | 30 – 45 万 |
| < 2.2 | — | 不建议推进 | — |

一线以外按城市系数折算(新一线 0.82、二线 0.68、其他 0.58)。

**薪资数字由 `skills/interviewer/scripts/score.mjs` 算,不靠模型心算。**
脚本是唯一口径来源,这样两次面试的结论才可比 —— 也让「同一份输入永远得到同一个分」
成为一条可验证的性质(见下面的自检)。

> 这是**总包**不是 Base,级差不是线性的,而且**模拟面试 ≠ 真实定级**。
> 真实结果还受城市、公司、团队缺口和议价能力影响。

---

## 面试状态机:为什么需要它

第一版是**无状态**的:每轮把整份对话丢给模型问「下一句问什么」。

结果它在同一个话题上**追了 5 轮**(SOP 只允许 2 轮),代价是没余力接别的话题 ——
放掉了一段教科书级的含糊回答(候选人说「JVM 调过一些参数,后来就好了」,
面试官因为还卡在上一个话题,整段放过去了)。

根因不是提示词写得不好,是**模型没有工作记忆**:它每轮都要从零推导「该问什么」,
而最近上下文里最抢眼的就是「他刚才没回答我」,所以它一直拉那根线。

所以状态被显式化了。分工很明确:

- **模型负责**判断这一轮在聊什么话题(它擅长这个)
- **状态机负责**数它追了几次、该不该换、还剩多少时间(机械计数,不该交给模型)

### 一条重要的设计教训

让模型每轮吐一个状态标记,**命中率只有 50% 上下**。换措辞、把协议挪到提示词最末尾,
都救不回来(挪末尾反而从 4/5 掉到 1/5)。

所以状态机**不依赖模型配合才能正确**:

- 到上限时,提示词会**点名**下一个话题(「本轮换到「数据库与扩展」」)——
  「换个话题」是抽象指令,模型大概率继续追问;点名它才动得起来
- 下一轮如果仍然没有标记,状态机就**乐观前进**,自己把话题指针推到那里
- 计划要不到就装一份按方向预设的骨架

结果是即使模型完全不吐标记,广度推进依然成立。

### 回归实测

`interview-sim/state-run.mjs`,11 轮真实模型调用,与无状态版**同一套候选人台词**:

| | 无状态版 | 状态机版 |
|---|---|---|
| 单话题最多追问 | 4 轮(「多出来的 1.5 秒」反复重问) | **2 轮** |
| 走过的不同话题 | 2 个 | **6 个** |
| 收尾表现 | `max-tokens` 挂掉 | 「好,我整理一下」 |

---

## 数据与隐私

- **简历和全部对话内容会发送给你在 DSH 里配置的模型服务商。** 这是它工作的前提,
  不要往里粘你不想外发的东西。
- 面试状态(计划 / 话题 / 观察)保存在**浏览器内存里**,每轮随请求回传给主机侧的
  `POST /interviewer/api/chat`。主机侧是无状态的纯函数 —— 不写数据库、不落盘,
  DSH 重启也不会留下面试记录(对话本身也随之丢失)。
- 插件不额外收集任何遥测。

---

## 开发

### 自检

```bash
node dev-check.mjs     # 插件结构与接线(含用桩真跑一遍浏览器侧模块)
node state-test.mjs    # 面试状态机(纯函数,可离线断言)
```

`dev-check.mjs` 不是"看起来对",是"真的执行了":它用 `node:vm` + 桩
`window.__ModuleLoader__` 把 `lib/client.js` 真的跑一遍,用桩 `ctx` 执行 `apply()`,
抓取它实际发出的每一条槽位注册,再逐条核对槽位名。还包含**发布前必查的自包含断言** ——
包内 SOP 是否齐全、`files`/`exports` 有没有漏掉 `skills`。这类 bug 在本机永远复现不了,
只在别人机器上炸,所以只能靠断言钉住。

### 迭代边界

**没有构建步骤。** `lib/client.js` 手写在 DSH 的浏览器模块格式里,改完直接生效:

| 改了什么 | 需要 |
|---|---|
| `lib/client.js` | 刷新浏览器 |
| `lib/index.js`、`lib/skill.js`、`lib/state.js`、`lib/prompt.js`、`package.json` | 重启 `dsh web` |
| `package.json` 的**依赖声明** | 重新 `dsh plugin add` |

只有真的动了依赖才需要重装。改代码不需要。

### 文件结构

```
package.json        包清单:dsh.bundle.patch + dsh.client 两个声明
cordis.patch.yml    插件行:装插件时 CLI 读它,把包接进 dsh.profile.bundles
lib/index.js        主机侧:HTTP 路由 /interviewer/api/{health,chat}
lib/prompt.js       系统提示词装配(纯 Node,可离线拼出来做断言)
lib/state.js        面试状态机(纯函数)
lib/skill.js        把 SOP 注册成运行时技能 + 定位包内 SOP 目录
lib/client.js       浏览器侧:悬浮窗、设置弹窗、状态条、语音输入
skills/interviewer/ 随包分发的面试 SOP(SKILL.md + references/ + score.mjs)
dev-check.mjs       插件自检
state-test.mjs      状态机单元测试
```

---

## 技术说明

### 它是怎么接到界面上的

DSH 的 Web 界面是一棵**插槽(slot)树**。插件不直接改界面,只能往**别人已经声明的槽位里
加条目** —— `register` 到未声明的槽位会直接抛错。所以这不是「改 DSH」,是「插进 DSH
预留的座位」。

本插件只用一个座位:

| 槽位 | 类型 | 作用 |
|---|---|---|
| `shell.overlay` | `list` | 全框架浮层,渲染一个可拖拽的悬浮窗口 |

`ctx.slots.inject(槽位名, 回调)` 是延迟注册,等槽位被声明之后再注入,所以不用担心加载顺序。

**为什么只有这一个入口:**

| 槽位 | 为什么不用 |
|---|---|
| `main` | **全屏接管**中间区域,用户进得去出不来。体验太霸道,而且挡住主会话 |
| `sidebar.panellist` | 注册**成功**(回读确认「账面 1 条,生效 1 条」),但侧边栏就是不渲染它。原因未定位,疑似 `dsh-better-sidebar` 介入那块区域的渲染 |

### 路由

| 端点 | 用途 |
|---|---|
| `GET /interviewer/api/health` | 探测:路由挂上没有、模型解析到没有、SOP 从哪个目录读的 |
| `POST /interviewer/api/chat` | 发对话,拿回复 |

浏览器 → 主机走一条普通 HTTP 路由,不走 DSH 的 Remote:Remote 要接 typert 协议
(host / client 各一份描述),而这里只需要一次「发过去、拿回来」。路由更直接,
**出问题 curl 就能验**。

### 额度这件事

**推理(thinking)token 也计入 `maxTokens`。** 所以这个数字不是「回复能有多长」,
而是「想 + 说总共能有多长」。

实测下来模型的思考长度波动极大 —— 同一段对话同一个额度,有时 3 秒答完,
有时思考 14 秒一个字都没吐就直接 `max-tokens`,面板上出现一个断掉的话轮。

所以有一层保护:**一轮如果没有产出任何内容,自动把额度翻倍重试一次**,
并追加一句「别想了直接说」。已经写出半句话的情况不重试 —— 那会得到一个前后不连贯的答案。

---

## 已知限制

- 只有右下角悬浮窗一个入口(原因见上)
- **评分报告不在面板里**。评分通过 SOP 在主会话里跑 `scripts/score.mjs` 得到,
  面板目前只负责面试对话
- 简历要手动粘贴,没有拖放区;PDF 读不出字
- `shell.overlay` 是 `root` scope,浮层里拿不到 `sessionId`,
  所以面试对话**不进入 DSH 的会话历史**
- 状态条上的话题名依赖模型偶尔吐出的标记,可能有一轮偏差(乐观前进的代价)

## 装插件之后 DSH 起不来?

见 [`docs/dev-notes.md`](docs/dev-notes.md)。那不是这个插件的问题,是
`dsh plugin add` 会跑一次 pnpm install,扰动 DSH 的自定义原生插件加载器。
文档里有失败签名和官方退路。

## License

[MIT](LICENSE)

Install

dsh plugin --profile web add github:ufiredong/dsh-interviewer

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source