Final Project / 11
部署与演示
前面 10 篇把系统的每一层都设计清楚了。这一篇把它跑起来——从 npm install 到完整的诊断流程。
CLI 入口
// src/index.ts
import { Command as Cmd } from 'commander';
import { createApp } from './app';
import chalk from 'chalk';
const program = new Cmd();
program
.name('interview-diag')
.description('面试诊断 Agent —— 诊断你的面试回答质量')
.version('0.1.0');
program
.command('start')
.description('启动交互式诊断会话')
.option('-m, --model <model>', '主力模型', 'claude-sonnet-4-20250514')
.option('--stt <provider>', 'STT 引擎 (whisper|funasr)', 'whisper')
.option('--offline', '离线模式(FunASR + 不联网)')
.action(async (opts) => {
const app = await createApp(opts);
await app.repl();
});
program
.command('diagnose <file>')
.description('直接诊断一个文件(非交互式)')
.option('-f, --format <format>', '输入格式 (transcript|audio)', 'transcript')
.option('-o, --output <path>', '报告输出路径')
.action(async (file, opts) => {
const app = await createApp(opts);
await app.diagnoseFile(file, opts);
});
program
.command('build-kb')
.description('构建知识库(从 learn-agent-interview 导入)')
.option('-d, --dir <path>', '面试题目录', '../learn-agent-interview')
.option('--db <path>', '数据库路径', './data/knowledge.db')
.action(async (opts) => {
const { buildKnowledgeBase } = await import('./knowledge/cli');
await buildKnowledgeBase({
interviewDir: opts.dir,
dbPath: opts.db,
openaiApiKey: process.env.OPENAI_API_KEY!,
});
});
program
.command('stats')
.description('查看知识库和历史诊断统计')
.action(async () => {
const app = await createApp({});
app.printStats();
});
program.parse();
App 主类:组装所有模块
// src/app.ts
import { QueryEngine } from './query-engine/engine';
import { createToolRegistry } from './tools';
import { createCommandParser } from './command';
import { SkillRegistry } from './skills/registry';
import { ContextManager } from './context/manager';
import { MemoryStore } from './memory/store';
import { PermissionGate } from './permission/gate';
import { SessionManager } from './session/manager';
import { CheckpointManager } from './session/checkpoint';
import { HookPipeline } from './hooks/pipeline';
import { Orchestrator } from './agent/orchestrator';
import { SubAgentRuntime } from './agent/sub-agent';
import { KnowledgeStore } from './knowledge/store';
import { KnowledgeSearch } from './knowledge/search';
import { handleInput } from './agent/loop';
import * as readline from 'readline';
import chalk from 'chalk';
export async function createApp(opts: any): Promise<App> {
// 1. 基础设施
const queryEngine = new QueryEngine({
anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
openaiApiKey: process.env.OPENAI_API_KEY!,
deepseekApiKey: process.env.DEEPSEEK_API_KEY!,
cachePath: './data/cache.db',
budget: { maxTotalCost: 2.0 },
});
// 2. 知识库
const knowledgeStore = new KnowledgeStore('./data/knowledge.db');
const knowledgeSearch = new KnowledgeSearch(knowledgeStore, process.env.OPENAI_API_KEY!);
// 3. Tools & Skills
const toolRegistry = createToolRegistry();
const skillRegistry = new SkillRegistry();
// 4. 治理层
const permissionGate = new PermissionGate();
const hooks = createDefaultHookPipeline(permissionGate, queryEngine);
// 5. 持久化
const memoryStore = new MemoryStore('./data/memory.db');
const sessionManager = new SessionManager('./data/sessions.db');
const checkpoints = new CheckpointManager(sessionManager.db, 5);
// 6. Sub-agent
const subAgentRuntime = new SubAgentRuntime(queryEngine, toolRegistry);
const orchestrator = new Orchestrator(subAgentRuntime, { maxConcurrency: 3 });
// 7. Command
const commandParser = createCommandParser();
return new App({
queryEngine, knowledgeSearch, toolRegistry, skillRegistry,
permissionGate, hooks, memoryStore, sessionManager, checkpoints,
orchestrator, commandParser, opts,
});
}
class App {
private deps: AppDependencies;
private session: Session | null = null;
constructor(deps: AppDependencies) {
this.deps = deps;
}
async repl(): Promise<void> {
this.printBanner();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk.cyan('> '),
});
rl.prompt();
rl.on('line', async (line) => {
const input = line.trim();
if (!input) { rl.prompt(); return; }
if (input === 'exit' || input === 'quit') { rl.close(); return; }
try {
if (!this.session) {
this.session = this.deps.sessionManager.create(
{ type: 'transcript', content: '' },
{ model: this.deps.opts.model }
);
}
await handleInput(input, this.session, this.deps);
} catch (err) {
console.error(chalk.red(`Error: ${err}`));
}
rl.prompt();
});
rl.on('close', () => {
console.log(chalk.dim('\n再见!'));
process.exit(0);
});
}
async diagnoseFile(filePath: string, opts: any): Promise<void> {
const session = this.deps.sessionManager.create(
{ type: opts.format, content: filePath },
{ model: this.deps.opts.model }
);
const skill = opts.format === 'audio'
? this.deps.skillRegistry.resolve('diagnose-audio')
: this.deps.skillRegistry.resolve('diagnose-transcript');
const content = await readFile(filePath, 'utf-8');
const result = await skill.execute(
{ rawInput: content, parsedArgs: { path: filePath } },
session.makeSkillContext(),
);
if (result.report) console.log(result.report);
if (opts.output) {
await writeFile(opts.output, result.report ?? JSON.stringify(result.result, null, 2));
console.log(chalk.green(`\n报告已保存: ${opts.output}`));
}
}
printStats(): void {
const kbStats = this.deps.knowledgeSearch.store.getStats();
const memStats = this.deps.memoryStore.getStats();
const sessions = this.deps.sessionManager.list({ limit: 100 });
console.log(chalk.bold('\n📊 系统统计\n'));
console.log(`知识库: ${kbStats.totalEntries} 题 / ${kbStats.dimensions} 维度 / ${kbStats.withEmbedding} 有向量`);
console.log(`记忆: ${memStats.total} 条 (${Object.entries(memStats.byType).map(([k, v]) => `${k}:${v}`).join(', ')})`);
console.log(`会话: ${sessions.length} 次 (完成:${sessions.filter(s => s.status === 'completed').length} / 暂停:${sessions.filter(s => s.status === 'paused').length})`);
}
private printBanner(): void {
console.log(chalk.bold(`
┌─────────────────────────────────────────────┐
│ 面试诊断 Agent v0.1.0 │
│ 上传面试稿或录音,获得结构化诊断报告 │
│ │
│ /help 查看命令 /mock 模拟面试 │
│ /status 查看状态 exit 退出 │
└─────────────────────────────────────────────┘
`));
}
}
环境配置
# .env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
DEEPSEEK_API_KEY=sk-...
// package.json
{
"name": "interview-diagnosis-agent",
"version": "0.1.0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts start",
"diagnose": "tsx src/index.ts diagnose",
"build-kb": "tsx src/index.ts build-kb",
"build": "tsc",
"test": "vitest",
"test:e2e": "vitest run tests/e2e"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0",
"openai": "^4.70.0",
"better-sqlite3": "^11.0.0",
"commander": "^12.0.0",
"chalk": "^5.3.0",
"ora": "^8.0.0"
},
"devDependencies": {
"typescript": "^5.5.0",
"vitest": "^2.0.0",
"@types/better-sqlite3": "^7.6.0",
"tsx": "^4.0.0"
}
}
首次运行流程
# 1. 安装依赖
pnpm install
# 2. 配置环境变量
cp .env.example .env
# 编辑 .env 填入 API Key
# 3. 构建知识库
pnpm build-kb --dir ../learn-agent-interview
# 4. 启动
pnpm start
Demo 演示:完整诊断流程
场景 1:文字稿诊断
> 我要诊断一份面试稿
好的,请粘贴面试内容,或使用 /upload <文件路径> 上传。
> /upload ./mock-interview.txt
[MEDIUM] 权限确认请求
原因: 读取用户本地文件
工具: read_file
path: ./mock-interview.txt
允许执行? (y/n): y
✓ 已读取文件 (3.2KB)
✓ 检测到格式: labeled(面试官:/候选人:)
✓ 拆分出 8 道 Q&A
开始诊断...
● Q1: "ReAct 和 Plan-and-Execute 怎么选"
├─ 知识库匹配: architecture-design:1 (相似度 0.94)
├─ 内容诊断: 62/100
│ completeness: 55 | depth: 68 | accuracy: 72 | practicality: 52
│ 关键遗漏: 没提到"检查点"机制,没区分不确定性
└─ 表达诊断: 71/100
structure: 65 | conciseness: 78 | keywords: 72 | closure: 68
问题: 没有先给结论再展开,开头犹豫
● Q2: "Tool Calling 失败怎么兜底"
├─ 知识库匹配: fault-tolerance:5 (相似度 0.91)
├─ 内容诊断: 78/100
└─ 表达诊断: 82/100
... (Q3-Q7) ...
● Q8: "多 Agent 之间怎么协调"
├─ 知识库匹配: multi-agent-collab:3 (相似度 0.87)
├─ 内容诊断: 45/100
│ 关键遗漏: 没提到上下文隔离、没区分编排模式
└─ 表达诊断: 58/100
问题: 回答没有结构,流水账式描述
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
诊断完成 · 8/8 题
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总分: 67/100
内容平均: 64 表达平均: 71
强项:
• 容错和兜底策略理解到位
• 技术术语使用准确
弱项:
• 架构选型缺乏场景化判断
• 多 Agent 部分回答太浅
• 表达缺乏框架感,开头常犹豫
改进建议:
[立即可改]
① 每题先给结论(1句话),再展开
② "我认为..."开头改为直接陈述
[短期提升]
③ 架构选型题练习"场景→约束→选择→代价"四步法
④ 多 Agent 重点补充"上下文隔离"和"编排策略"
[长期积累]
⑤ 补充实际项目中的 Agent 工程经验
资源使用:
Requests: 24 | Tokens: 38,200in + 12,400out | Cost: $0.31
> /export md ./my-report.md
报告已导出: ./my-report.md (md)
场景 2:录音诊断
> /upload ./interview-recording.m4a
[MEDIUM] 权限确认请求
原因: 将上传音频到外部 STT 服务处理
工具: transcribe_audio
允许执行? (y/n): y
⠋ 预处理音频...
✓ 格式: m4a → 16kHz WAV (28.3MB)
⠋ STT 转写中... (预计 30s)
✓ 转写完成: 32分钟 / 5,420 字
✓ 说话人分离: 面试官 12 段 / 候选人 12 段
✓ 拆分出 12 道 Q&A
开始诊断(含语音分析)...
● Q1: "Agent 的记忆系统怎么设计"
├─ 内容: 58/100
├─ 表达: 62/100
└─ 语音: 55/100
语速 180字/分(偏慢) | 填充词 8 次 | 长停顿 2 处
... (Q2-Q12) ...
总分: 61/100 (内容 60 / 表达 65 / 语音 58)
语音整体建议:
• 平均填充词 6.5 次/题——目标 < 3 次
• 3 处 >4s 的长停顿出现在架构题——建议提前准备框架
• 语速后半段明显加快——可能是紧张导致
> /detail 1
[第 1 题详细诊断]
问题: Agent 的记忆系统怎么设计
你的回答:
"嗯...那个...记忆系统的话,我觉得就是分成短期和长期的。
短期就是当前对话的内容,长期就是...嗯...比如用户的偏好什么的。
然后可以用向量数据库存,检索的时候用 embedding 匹配..."
高手答(知识库参考):
"记忆分三层:工作记忆(当前任务状态)、情景记忆(历史交互摘要)、
语义记忆(长期事实和偏好)。关键设计点不在存储,而在两个动作:
什么时候写入、什么时候召回。写入要克制,否则变成噪声仓库..."
差距:
① 你只说了"短期/长期"二分法,缺少"工作记忆"这个关键概念
② 没提"写入克制"原则——面试官最想听的工程 insight
③ 表达上以"嗯...那个..."开头,缺乏自信感
场景 3:模拟面试
> /mock memory --count 3
模拟面试开始!共 3 道题,维度:记忆与上下文
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第 1 题
Agent 中的"长短期记忆"分别存什么、怎么协调?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
请回答(输入答案,/skip 跳过):
> 短期记忆存当前对话上下文,长期记忆存用户偏好和历史事实。协调的话,每次对话开始时从长期记忆里检索相关内容注入短期记忆。
[即时反馈]
得分: 55/100
问题:
• 缺少"工作记忆"概念(当前任务的中间状态)
• "协调"只说了检索注入,没说写入时机和淘汰策略
• 表述太简单,没有递进分析
升级建议:
分三层说:工作记忆(任务态)→ 情景记忆(交互摘要)→ 语义记忆(事实)
重点讲"什么时候写入"和"什么时候召回"两个动作
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第 2 题
...
部署方案
方案 A:本地 CLI(推荐开发阶段)
优点: 隐私安全、开发调试方便、无需服务器
缺点: 需要 Node.js 环境、API Key 在本地
适合: 个人使用、开发迭代
# 全局安装
npm install -g interview-diagnosis-agent
interview-diag start
方案 B:Docker 容器
FROM node:20-slim
RUN apt-get update && apt-get install -y ffmpeg python3 pip
RUN pip install funasr modelscope
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
VOLUME ["/app/data"]
ENV NODE_ENV=production
ENTRYPOINT ["node", "dist/index.js"]
CMD ["start"]
docker build -t interview-diag .
docker run -it \
-v ./data:/app/data \
--env-file .env \
interview-diag
方案 C:Web 服务(未来扩展)
前端: Next.js + 录音组件 + 实时流式输出
后端: 本项目 CLI 改造为 HTTP Server(Express/Fastify)
部署: Vercel (前端) + Railway/Fly.io (后端)
接口设计:
POST /api/diagnose 上传文字稿 → SSE 流式诊断
POST /api/upload-audio 上传录音 → 转写 + 诊断
GET /api/sessions 会话列表
GET /api/report/:id 获取报告
POST /api/mock/start 开始模拟面试
POST /api/mock/answer 提交回答
这是 Phase 5 的扩展方向,当前 MVP 聚焦 CLI。
测试策略
单元测试 (vitest):
├── query-engine/ stream 解析、retry 逻辑、cache 命中
├── tools/ 每个 tool 的输入验证和输出格式
├── context/ 压缩逻辑、token 估算
├── permission/ 规则匹配、缓存
├── session/ 状态机转换、checkpoint
├── hooks/ 管线执行顺序、block/modify
└── knowledge/ FTS 检索、embedding 相似度
集成测试:
├── 知识库导入 → 检索 → 命中
├── Agent Loop: 输入 → tool_use → result → 输出
├── Session: create → process → pause → resume
└── 完整诊断流程 (mock LLM 响应)
E2E 测试(需要真实 API Key):
├── 文字稿诊断全流程
├── 录音诊断全流程
└── 模拟面试全流程
项目总结:从 Harness 工程到完整产品
graph TB
subgraph "用户界面"
CLI[Commander.js CLI]
CMD[Command Layer: 14 命令]
end
subgraph "Agent Runtime"
LOOP[Agent Loop]
DISPATCH[Dispatcher]
ORCH[Orchestrator]
SUB[Sub-agents × 4]
end
subgraph "治理层"
HOOKS[Hook Pipeline: 7 hooks]
PERM[Permission Gate]
CTX[Context Manager: 5 层]
MEM[Memory Store]
SESS[Session + Checkpoint]
end
subgraph "能力层"
SKILLS[Skills: 4 技能]
TOOLS[Tools: 8 工具]
QE[Query Engine: 3 Provider]
end
subgraph "基础设施"
KB[(Knowledge Base: 385 题)]
STT[STT: Whisper / FunASR]
DB[(SQLite: session/memory/cache/audit)]
end
CLI --> CMD --> LOOP
LOOP --> DISPATCH --> HOOKS --> TOOLS
LOOP --> ORCH --> SUB
LOOP --> CTX
CTX --> MEM
DISPATCH --> PERM
TOOLS --> QE
TOOLS --> KB
TOOLS --> STT
SESS --> DB
MEM --> DB
QE --> DB
10 层 Harness 对照:
| 层 | 本项目实现 | 文档 |
|---|---|---|
| 1. Tools | 8 个原子工具,标准 schema | 04 |
| 2. Skills | 4 个任务级流程 | 04 |
| 3. Query Engine | 3 Provider + stream + retry + cache + 路由 | 03 |
| 4. Context | 5 层分区 + 3 级压缩 | 06 |
| 5. Memory | SQLite + 画像 + 弱点追踪 + 触发器 | 06 |
| 6. Permission | 风险分级 + human-in-the-loop + 审计 | 07 |
| 7. Sessions | 状态机 + checkpoint + rewind | 07 |
| 8. Command | 14 个确定性命令 | 08 |
| 9. Hook | 7 个 pre/post hooks 管线 | 08 |
| 10. Sub-agent | 4 个角色 + 并发池 + 上下文隔离 | 09 |
小结
- CLI 是 MVP 的最佳交互形式:快速、确定、无前端依赖
- App 主类把 10 个模块组装起来,REPL 循环驱动整个系统
- 首次运行三步:install → build-kb → start
- 三种部署方案:本地 CLI(开发)→ Docker(分享)→ Web 服务(产品化)
- 测试分三层:单元 → 集成 → E2E,覆盖所有模块
- 完整实现了 Harness 10 层架构——不是 Demo,是可恢复、可审计、可扩展的工程系统
这是 Final Project 系列的最后一篇。
从 PRD 到架构、从 Query Engine 到 Sub-agent、从知识库到语音分析——11 篇文档完整记录了如何从 0 搭建一个生产级面试诊断 Agent。
核心教训只有一个:Agent 的主战场不在模型,在 Harness。