← 全部示例
// 场景实例

个性化面试模拟器(面试官 + 观察者)

进行多轮面试,让独立观察者追踪完整对话,最后生成结构化复盘。

适合原型验证面试练习与结构化辅导的团队

01 使用情境

面试官需要保持对话连续性,评估者则需要一定距离,才能观察整场会话的模式。把两个角色塞进同一个 prompt,容易让面试过程不稳定。

候选人材料与实时回答

内置材料可以替换成你自己的简历、项目笔记、代码和职位描述目录。

02 工作方式

一支分工明确、交接清楚的团队。

每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。

  1. 进行面试

    面试官跨轮保留状态,并接收候选人在终端输入的回答。

    面试官

    根据先前回答与候选人材料继续追问。

  2. 在轮次之间观察

    无状态观察者每轮重新读取完整转录。

    面试观察者

    记录模式与辅导信号,但不接管对话。

  3. 以复盘结束

    完整会话被转换为通过 schema 校验的结果。

    复盘步骤

    在面试循环结束时生成结构化反馈。

03 最终结果

个性化、结构化的面试复盘

对话仍由人推动,观察者则从完整转录提供第二视角。

  1. 01有状态的多轮面试
  2. 02跨轮观察记录
  3. 03通过 schema 校验的最终复盘
  • 对话与评估职责分离。
  • 共享记忆让候选人背景保持可用。
  • 结构化复盘可以继续进入辅导流程。
范围与限制
这是交互式模拟器,不是经过验证的招聘评估。候选人材料与转录可能包含敏感信息,生产环境必须明确保留和访问政策。
04 开发者实现

先运行,再查看具体实现。

上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。

实现信息 AgentSharedMemoryToolExecutorToolRegistry
场景实例 274 行

运行

在仓库的克隆里运行这个文件:

terminal
npx tsx packages/core/examples/cookbook/personalized-interview-simulator.ts
前置条件
  • ANTHROPIC_API_KEY env var must be set.

OMA 与 provider 无关——这个示例按上面的 key 编写,但你也可以用 OpenAI、Gemini、Groq 等任意 provider 运行。 全部 provider →

展开完整同步源码 · 274 行

完整示例,从固定的 Framework commit 同步。

cookbook/personalized-interview-simulator.ts
/**
* Personalized Interview Simulator (Interviewer + Observer)
*
* Demonstrates:
* - A stateful `interviewer` agent using `Agent.prompt()` across turns
* - A stateless `observer` agent that reads full transcript context between turns
* - Manual `SharedMemory` seeding and prompt injection outside `runTeam()` / `runTasks()`
* - Human input handled at app level with `readline`
* - Structured debrief via Zod at the end of the interview loop
*
* Run:
* npx tsx packages/core/examples/cookbook/personalized-interview-simulator.ts
*
* Prerequisites:
* ANTHROPIC_API_KEY env var must be set.
*
* Optional:
* Set INTERVIEW_CANDIDATE_DIR to point at your own materials directory.
* Expected files:
* - resume.md
* - project-notes.md
* - code.ts
* - job-description.md
*/
 
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { createInterface } from 'node:readline/promises'
import { stdin as input, stdout as output } from 'node:process'
import { fileURLToPath } from 'node:url'
import { z } from 'zod'
import { Agent, SharedMemory, ToolExecutor, ToolRegistry } from '../../src/index.js'
import type { AgentConfig } from '../../src/types.js'
 
// ---------------------------------------------------------------------------
// Candidate materials
// ---------------------------------------------------------------------------
 
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const DEFAULT_CANDIDATE_DIR = path.join(__dirname, '../fixtures/interview-candidate')
const CANDIDATE_DIR = process.env.INTERVIEW_CANDIDATE_DIR ?? DEFAULT_CANDIDATE_DIR
const MAX_TURNS = 10
 
function loadText(fileName: string): string {
return readFileSync(path.join(CANDIDATE_DIR, fileName), 'utf-8').trim()
}
 
const resumeText = loadText('resume.md')
const projectNotes = loadText('project-notes.md')
const codeSnippet = loadText('code.ts')
const jobDescription = loadText('job-description.md')
 
// ---------------------------------------------------------------------------
// Structured debrief
// ---------------------------------------------------------------------------
 
const DebriefSchema = z.object({
questions_asked: z.array(
z.object({
question: z.string(),
why_it_mattered: z.string(),
}),
),
weak_spots: z.array(z.string()),
strong_spots: z.array(z.string()),
overall_assessment: z.object({
recommendation: z.enum(['strong-hire', 'hire', 'lean-hire', 'lean-no-hire', 'no-hire']),
summary: z.string(),
}),
})
type Debrief = z.infer<typeof DebriefSchema>
 
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
 
function buildAgent(config: AgentConfig): Agent {
const registry = new ToolRegistry()
const executor = new ToolExecutor(registry)
return new Agent(config, registry, executor)
}
 
async function readlineFromCandidate(prompt: string, rl: ReturnType<typeof createInterface>): Promise<string> {
console.log('\n' + '='.repeat(60))
console.log('INTERVIEWER')
console.log('='.repeat(60))
console.log(prompt)
console.log()
 
const answer = await rl.question('Candidate > ')
return answer.trim()
}
 
function isExitAnswer(answer: string): boolean {
const normalized = answer.trim().toLowerCase()
return normalized === 'exit' || normalized === 'quit'
}
 
// ---------------------------------------------------------------------------
// Agent configs
// ---------------------------------------------------------------------------
 
if (!process.env.ANTHROPIC_API_KEY) {
console.log('[skip] This example needs ANTHROPIC_API_KEY.')
process.exit(0)
}
 
const interviewerConfig: AgentConfig = {
name: 'interviewer',
provider: 'anthropic',
model: 'claude-sonnet-4-6',
systemPrompt: `You are a senior technical interviewer.
You are running a personalized interview grounded in:
- the candidate's resume
- project notes
- a code sample
- the target job description
- observer flags written after each turn
Rules:
- Ask exactly one question per turn.
- Prefer probing follow-ups over switching topics too early.
- Use the role expectations and candidate materials together.
- If the observer flags a contradiction, vague answer, or missing dimension, use that.
- Keep each question concise but sharp.
- Do not answer your own question.
- Do not output analysis, just the next interview question.`,
maxTurns: 2,
temperature: 0.3,
}
 
const observerConfig: AgentConfig = {
name: 'observer',
provider: 'anthropic',
model: 'claude-sonnet-4-6',
systemPrompt: `You are an interview observer.
You will receive shared memory containing:
- candidate resume
- project notes
- code sample
- target job spec
- the full interview transcript so far
Write compact flags for the next interviewer turn.
Focus on:
- contradictions with candidate claims
- vague or incomplete answers
- important dimensions not yet tested
- specific follow-up hooks hidden in the answer
Output 3-6 short bullets. Be concrete. No prose introduction.`,
maxTurns: 1,
temperature: 0.2,
}
 
const reporterConfig: AgentConfig = {
name: 'reporter',
provider: 'anthropic',
model: 'claude-sonnet-4-6',
systemPrompt: `You are writing a compact interview debrief.
Use the complete interview record to produce:
- questions_asked: the most important questions and why each mattered
- weak_spots: concise bullets for weak areas
- strong_spots: concise bullets for strengths
- overall_assessment: recommendation plus a short summary
Return JSON only, matching the schema exactly.`,
maxTurns: 2,
temperature: 0.1,
outputSchema: DebriefSchema,
}
 
// ---------------------------------------------------------------------------
// Seed memory
// ---------------------------------------------------------------------------
 
const mem = new SharedMemory()
 
await mem.write('candidate', 'resume', resumeText)
await mem.write('candidate', 'project-notes', projectNotes)
await mem.write('candidate', 'code', codeSnippet)
await mem.write('role', 'target-job-spec', jobDescription)
 
const interviewer = buildAgent(interviewerConfig)
const observer = buildAgent(observerConfig)
const reporter = buildAgent(reporterConfig)
 
const rl = createInterface({ input, output })
 
console.log('Personalized Interview Simulator')
console.log('='.repeat(60))
console.log(`Candidate materials: ${CANDIDATE_DIR}`)
console.log('Type "exit" or "quit" to stop early.')
console.log()
 
// ---------------------------------------------------------------------------
// Interactive loop
// ---------------------------------------------------------------------------
 
let answer = ''
let turnsCompleted = 0
 
try {
for (let turn = 0; turn < MAX_TURNS; turn++) {
const ctx = await mem.getSummary()
const result = await interviewer.prompt(
turn === 0
? [
`Context:\n${ctx}`,
'Ask the most probing opening question you can justify from the materials and role.',
].join('\n\n')
: [
`Context:\n${ctx}`,
`Candidate answer:\n${answer}`,
'Ask the next question.',
].join('\n\n'),
)
 
if (!result.success) {
console.error('Interviewer failed:', result.output)
process.exit(1)
}
 
answer = await readlineFromCandidate(result.output, rl)
if (isExitAnswer(answer)) {
console.log('\nInterview stopped by candidate.')
break
}
 
turnsCompleted++
await mem.write('interviewer', `turn-${turn}`, `Q: ${result.output}\nA: ${answer}`)
 
const observerResult = await observer.run(
[
'Review transcript and candidate materials. Write flags for the next turn.',
await mem.getSummary(),
].join('\n\n'),
)
 
if (!observerResult.success) {
console.error('Observer failed:', observerResult.output)
process.exit(1)
}
 
await mem.write('observer', 'flags', observerResult.output)
}
} finally {
rl.close()
}
 
// ---------------------------------------------------------------------------
// Structured debrief
// ---------------------------------------------------------------------------
 
console.log('\n' + '='.repeat(60))
console.log('DEBRIEF')
console.log('='.repeat(60))
 
const debriefResult = await reporter.run(`Summarize the full interview:\n\n${await mem.getSummary()}`)
 
if (!debriefResult.success || !debriefResult.structured) {
console.error('Debrief generation failed:', debriefResult.output)
process.exit(1)
}
 
const debrief = debriefResult.structured as Debrief
 
console.log(JSON.stringify(debrief, null, 2))
console.log()
console.log(`Turns completed: ${turnsCompleted}`)
console.log('Done.')
在 GitHub 查看 / 编辑
// 企业服务

要把它用到生产环境?

open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。

// 直接联系

把 Open Multi-Agent 用进真实业务

联系框架作者本人,帮你梳理 AI 落地目标、让 AI 真正与业务结合

可提供的工程服务
S-01

AI Agent 定制开发

业务梳理、Agent 设计、Prompt 评估、生产部署、私有化与持续支持。

S-02

多智能体系统集成

多 Agent 架构编排、RAG、CRM / ERP / API 对接、性能与稳定性调优。

S-03

企业 AI 咨询

AI 场景评估、技术选型、POC、ROI 估算与落地路线规划。