要解决的问题
面试官需要保持对话连续性,评估者则需要一定距离,才能观察整场会话的模式。把两个角色塞进同一个 prompt,容易让面试过程不稳定。
进行多轮面试,让独立观察者追踪完整对话,最后生成结构化复盘。
适合原型验证面试练习与结构化辅导的团队
面试官需要保持对话连续性,评估者则需要一定距离,才能观察整场会话的模式。把两个角色塞进同一个 prompt,容易让面试过程不稳定。
内置材料可以替换成你自己的简历、项目笔记、代码和职位描述目录。
每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。
面试官跨轮保留状态,并接收候选人在终端输入的回答。
根据先前回答与候选人材料继续追问。
无状态观察者每轮重新读取完整转录。
记录模式与辅导信号,但不接管对话。
完整会话被转换为通过 schema 校验的结果。
在面试循环结束时生成结构化反馈。
对话仍由人推动,观察者则从完整转录提供第二视角。
上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。
AgentSharedMemoryToolExecutorToolRegistry 在仓库的克隆里运行这个文件:
npx tsx packages/core/examples/cookbook/personalized-interview-simulator.ts
OMA 与 provider 无关——这个示例按上面的 key 编写,但你也可以用 OpenAI、Gemini、Groq 等任意 provider 运行。 全部 provider →
完整示例,从固定的 Framework commit 同步。
/*** 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_DIRconst MAX_TURNS = 10function 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 turnRules:- 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 farWrite 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 answerOutput 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 summaryReturn 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 = 0try {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 Debriefconsole.log(JSON.stringify(debrief, null, 2))console.log()console.log(`Turns completed: ${turnsCompleted}`)console.log('Done.')
open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。