要解决的问题
患者自述、科普材料、指南、遗传证据和商业论断的可靠性完全不同。过早混合可能把微弱重叠变成虚假确定性,甚至导向商业推荐。
把五类证据保持隔离,暴露冲突与夸大论断,并产出受安全约束的信息分诊结果。
适合评估健康信息系统安全模式的团队
患者自述、科普材料、指南、遗传证据和商业论断的可靠性完全不同。过早混合可能把微弱重叠变成虚假确定性,甚至导向商业推荐。
虚构的患者症状、公益科普、指南类内容、基因—表型证据,以及网络或商业论断。
每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。
五位审查者独立工作,让来源与质量始终可见。
整理自述症状,但不做诊断。
核对公益科普论断。
保留官方类指南中的宽泛鉴别范围。
记录微弱或不确定的基因—表型证据。
标记确定性夸大和付费检测推广。
下游仲裁者接收结构化审查,而不是混合后的原始来源。
检测冲突,并禁止诊断、治疗、剂量或商业推荐。
结果传达不确定性与来源分歧,不会把材料转换成医疗建议。
上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。
AgentToolExecutorToolRegistry 在仓库的克隆里运行这个文件:
npx tsx packages/core/examples/cookbook/rare-disease-information-triage.ts
OMA 与 provider 无关——这个示例按上面的 key 编写,但你也可以用 OpenAI、Gemini、Groq 等任意 provider 运行。 全部 provider →
完整示例,从固定的 Framework commit 同步。
/*** Rare Disease Information Triage (Source-Isolated Evidence Audit + Safety Arbitration)** Demonstrates:* - Five source-isolated agents reading different MOCK fixtures* - Source-isolated evidence audits over patient-reported symptoms, nonprofit education,* official guideline/expert-consensus style content, gene-phenotype evidence,* and web/forum/commercial claims* - A downstream arbiter that receives only structured audit outputs* - Runtime conflict detection:* - nonprofit content may make symptom overlap look specific* - official guidance keeps the differential broad* - gene-phenotype evidence is weak/uncertain for the claimed disease* - web/commercial content overstates certainty and promotes a paid test* - safety policy forbids diagnosis, treatment, dosing, or commercial recommendation* - Zod-validated structured output and simple runtime assertions** Run:* npx tsx packages/core/examples/cookbook/rare-disease-information-triage.ts** Prerequisites:* ANTHROPIC_API_KEY env var must be set.* Requires Node.js >= 18.** Fixtures:* All fixtures under examples/fixtures/rare-disease-information-triage/ are MOCK.* They are shaped like realistic source types but do not contain real patient data* and do not provide medical diagnosis or treatment advice.*/import { readFileSync } from 'node:fs'import { fileURLToPath } from 'node:url'import path from 'node:path'import { z } from 'zod'import { Agent, ToolExecutor, ToolRegistry } from '../../src/index.js'import type { AgentConfig, AgentRunResult } from '../../src/types.js'// ---------------------------------------------------------------------------// Fixture loading// ---------------------------------------------------------------------------const __dirname = path.dirname(fileURLToPath(import.meta.url))const fixtureRoot = path.join(__dirname, '../fixtures/rare-disease-information-triage')function readFixture(name: string): string {return readFileSync(path.join(fixtureRoot, name), 'utf-8')}const patientSymptomSummary = readFixture('patient-symptom-summary.json')const nonprofitPatientEducation = readFixture('nonprofit-patient-education.md')const officialGuidelineExcerpt = readFixture('official-guideline-excerpt.md')const genePhenotypeEvidence = readFixture('gene-phenotype-evidence.json')const webClaimsSnippets = readFixture('web-claims-snippets.json')const medicalSafetyPolicy = readFixture('medical-safety-policy.json')const MODEL = process.env['MODEL'] ?? 'claude-sonnet-4-6'const ANTHROPIC_API_KEY = process.env['ANTHROPIC_API_KEY']const ANTHROPIC_BASE_URL = process.env['ANTHROPIC_BASE_URL']const providerConfig = {provider: 'anthropic' as const,model: MODEL,apiKey: ANTHROPIC_API_KEY,baseURL: ANTHROPIC_BASE_URL,tools: [] as const,}// ---------------------------------------------------------------------------// Zod schemas// ---------------------------------------------------------------------------const SymptomAudit = z.object({source_is_mock: z.boolean(),symptom_clusters: z.array(z.string()),red_flags: z.array(z.string()),claimed_condition_from_patient_context: z.string(),missing_clinical_information: z.array(z.string()),diagnosis_attempted: z.boolean(),})type SymptomAudit = z.infer<typeof SymptomAudit>const NonprofitEducationAudit = z.object({source_is_mock: z.boolean(),educational_claims: z.array(z.string()),simplification_risks: z.array(z.string()),patient_friendly_but_incomplete_points: z.array(z.string()),claims_that_may_overfocus_on_one_condition: z.array(z.string()),})type NonprofitEducationAudit = z.infer<typeof NonprofitEducationAudit>const GuidelineAudit = z.object({source_is_mock: z.boolean(),candidate_disease_families: z.array(z.string()),specialist_evaluation_required: z.boolean(),reasons_symptoms_are_not_specific: z.array(z.string()),recommended_non_diagnostic_next_steps: z.array(z.string()),})type GuidelineAudit = z.infer<typeof GuidelineAudit>const GeneticsAudit = z.object({source_is_mock: z.boolean(),claimed_gene_or_marker: z.string(),evidence_strength_for_claimed_disease: z.enum(['strong', 'moderate', 'weak', 'uncertain']),phenotype_consistency: z.enum(['consistent', 'partially_consistent', 'inconsistent', 'uncertain']),uncertainty_flags: z.array(z.string()),})type GeneticsAudit = z.infer<typeof GeneticsAudit>const WebClaimsAudit = z.object({source_is_mock: z.boolean(),claims: z.array(z.object({source_label: z.string(),claim: z.string(),claim_type: z.enum(['forum_experience', 'general_web_content', 'commercial_promotion']),overstatement_risk: z.enum(['low', 'moderate', 'high']),commercial_intent: z.boolean(),unsupported_elements: z.array(z.string()),})),})type WebClaimsAudit = z.infer<typeof WebClaimsAudit>const SafetyAudit = z.object({source_is_mock: z.boolean(),prohibited_outputs: z.array(z.string()),allowed_outputs: z.array(z.string()),required_disclaimers: z.array(z.string()),patient_facing_answer_allowed_without_clinician: z.boolean(),})type SafetyAudit = z.infer<typeof SafetyAudit>const TriageDecision = z.object({decision: z.enum(['credible_lead', 'conflicting_evidence', 'misleading_or_commercial', 'needs_specialist_review']),confidence: z.enum(['low', 'medium', 'high']),conflicts: z.array(z.string()),missing_evidence: z.array(z.string()),unsafe_elements: z.array(z.string()),safe_next_steps: z.array(z.string()),patient_facing_answer_allowed: z.boolean(),diagnosis_provided: z.boolean(),rationale: z.string(),})type TriageDecision = z.infer<typeof TriageDecision>// ---------------------------------------------------------------------------// Agent configs// ---------------------------------------------------------------------------const symptomNormalizerConfig: AgentConfig = {name: 'symptom-normalizer',...providerConfig,systemPrompt: `You are a medical information triage assistant.You audit only a MOCK patient-reported symptom summary. You must not diagnose.Extract symptom clusters, red flags, the patient's suspected condition from context,and missing clinical information.Return JSON matching the provided schema. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose. Do not provide diagnosis or treatment advice.`,maxTurns: 1,maxTokens: 1800,temperature: 0.1,outputSchema: SymptomAudit,}const nonprofitEducationConfig: AgentConfig = {name: 'nonprofit-education-auditor',...providerConfig,systemPrompt: `You audit only MOCK nonprofit / patient-advocacy educational content.Your job is to identify what the patient-friendly content says, what it simplifies,and whether the simplification may make symptom overlap look more disease-specificthan it is. Do not use outside medical knowledge.Return JSON matching the provided schema. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose.`,maxTurns: 1,maxTokens: 1800,temperature: 0.1,outputSchema: NonprofitEducationAudit,}const guidelineAuditorConfig: AgentConfig = {name: 'guideline-auditor',...providerConfig,systemPrompt: `You audit only a MOCK official disease guideline or expert-consensus style excerpt.Identify the broad differential, why symptoms are not specific, whether specialistevaluation is required, and safe non-diagnostic next steps. Do not diagnose.Return JSON matching the provided schema. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose.`,maxTurns: 1,maxTokens: 1800,temperature: 0.1,outputSchema: GuidelineAudit,}const geneticsAuditorConfig: AgentConfig = {name: 'genetics-auditor',...providerConfig,systemPrompt: `You audit only MOCK gene-phenotype evidence.Assess whether the claimed gene or marker strongly supports the claimed rare disease,whether the phenotype is consistent, and what uncertainty flags exist. Do not diagnoseor interpret real patient genetics.Return JSON matching the provided schema. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose.`,maxTurns: 1,maxTokens: 1800,temperature: 0.1,outputSchema: GeneticsAudit,}const webClaimsAuditorConfig: AgentConfig = {name: 'web-claims-auditor',...providerConfig,systemPrompt: `You audit only MOCK web, forum, and commercial claim snippets.Classify each claim as forum experience, general web content, or commercial promotion.Flag overstatement risk, commercial intent, and unsupported elements. Do not validateclaims using outside knowledge.Return JSON matching the provided schema. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose.`,maxTurns: 1,maxTokens: 2000,temperature: 0.1,outputSchema: WebClaimsAudit,}const safetyBoundaryConfig: AgentConfig = {name: 'safety-boundary-agent',...providerConfig,systemPrompt: `You audit only a MOCK medical safety policy.Extract prohibited outputs, allowed outputs, required disclaimers, and whether apatient-facing answer is allowed without a clinician. Do not generate medical advice.Return JSON matching the provided schema. Output ONLY valid JSON, no markdown, no code fences, and no explanatory prose.`,maxTurns: 1,maxTokens: 1800,temperature: 0.1,outputSchema: SafetyAudit,}const arbiterConfig: AgentConfig = {name: 'rare-disease-triage-arbiter',...providerConfig,systemPrompt: `You are the downstream arbiter for a rare disease information triage workflow.You receive only structured outputs from source-isolated audits. You cannot accessthe original fixtures. Your tasks:1. Detect conflicts among the audits.2. Identify misleading or commercial overclaims.3. Enforce the safety boundary.4. Decide whether the information is a credible lead, conflicting evidence,misleading/commercial content, or needs specialist review.5. Never provide diagnosis, treatment advice, dosing, or commercial recommendation.Return JSON matching the TriageDecision schema. The seeded conflict should be explicit:web/commercial content maps symptom similarity to one rare disease, while the guidelinekeeps the differential broad and genetics evidence is weak or uncertain.`,maxTurns: 1,maxTokens: 3000,temperature: 0.1,outputSchema: TriageDecision,}// ---------------------------------------------------------------------------// Build agents// ---------------------------------------------------------------------------function buildAgent(config: AgentConfig): Agent {// Source isolation is enforced at runtime: fixtures are injected into prompts,// and audit agents receive an empty tool registry with tools: [].const registry = new ToolRegistry()const executor = new ToolExecutor(registry)return new Agent(config, registry, executor)}const symptomNormalizer = buildAgent(symptomNormalizerConfig)const nonprofitEducationAuditor = buildAgent(nonprofitEducationConfig)const guidelineAuditor = buildAgent(guidelineAuditorConfig)const geneticsAuditor = buildAgent(geneticsAuditorConfig)const webClaimsAuditor = buildAgent(webClaimsAuditorConfig)const safetyBoundaryAgent = buildAgent(safetyBoundaryConfig)const triageArbiter = buildAgent(arbiterConfig)// ---------------------------------------------------------------------------// Main execution// ---------------------------------------------------------------------------console.log('Rare Disease Information Triage - Source-Isolated Audit + Safety Arbitration')console.log('='.repeat(88))console.log(`Model backend: anthropic-compatible (${MODEL})`)console.log('All fixtures are MOCK and contain no real patient data.')console.log('Expected runtime conflict: simplified patient-facing content + commercial claim vs broad guideline differential + weak genetics evidence.\n')async function runTimed(name: string,agent: Agent,prompt: string,): Promise<{ name: string, result: AgentRunResult, elapsedMs: number }> {const start = performance.now()const maxAttempts = 3for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {console.log(` [RUN] ${name} started... attempt ${attempt}/${maxAttempts}`)const result = await agent.run(prompt)const elapsedMs = performance.now() - startif (result.success && result.structured) {console.log(` [DONE] ${name} finished in ${Math.round(elapsedMs)}ms`)return { name, result, elapsedMs }}const outputPreview = result.output ? result.output.slice(0, 240).replace(/\s+/g, ' ') : '<empty output>'console.warn(` [RETRY] ${name} failed structured output on attempt ${attempt}/${maxAttempts}: ${outputPreview}`)}const result = await agent.run(prompt)const elapsedMs = performance.now() - startconsole.log(` [DONE] ${name} finished in ${Math.round(elapsedMs)}ms`)return { name, result, elapsedMs }}function requireStructured<T>(name: string, result: AgentRunResult): T {if (!result.success) {console.error(`${name} failed: ${result.output}`)process.exit(1)}if (!result.structured) {console.error(`${name} did not return structured output`)process.exit(1)}return result.structured as T}console.log('[Phase 1] Source-isolated audits\n')const auditStart = performance.now()const auditRuns: Array<{ name: string, result: AgentRunResult, elapsedMs: number }> = []auditRuns.push(await runTimed('symptom-normalizer',symptomNormalizer,`Audit this MOCK patient-reported symptom summary. Use only this input.\n\n${patientSymptomSummary}`,))auditRuns.push(await runTimed('nonprofit-education-auditor',nonprofitEducationAuditor,`Audit this MOCK nonprofit / patient-advocacy educational content. Use only this input.\n\n${nonprofitPatientEducation}`,))auditRuns.push(await runTimed('guideline-auditor',guidelineAuditor,`Audit this MOCK official guideline / expert-consensus style excerpt. Use only this input.\n\n${officialGuidelineExcerpt}`,))auditRuns.push(await runTimed('genetics-auditor',geneticsAuditor,`Audit this MOCK gene-phenotype evidence. Use only this input.\n\n${genePhenotypeEvidence}`,))auditRuns.push(await runTimed('web-claims-auditor',webClaimsAuditor,`Audit these MOCK web/forum/commercial claim snippets. Use only this input.\n\n${webClaimsSnippets}`,))auditRuns.push(await runTimed('safety-boundary-agent',safetyBoundaryAgent,`Audit this MOCK medical safety policy. Use only this input.\n\n${medicalSafetyPolicy}`,))const auditElapsed = performance.now() - auditStartconst auditMap = new Map(auditRuns.map((run) => [run.name, run.result]))const symptomAudit = requireStructured<SymptomAudit>('symptom-normalizer', auditMap.get('symptom-normalizer')!)const nonprofitAudit = requireStructured<NonprofitEducationAudit>('nonprofit-education-auditor', auditMap.get('nonprofit-education-auditor')!)const guidelineAudit = requireStructured<GuidelineAudit>('guideline-auditor', auditMap.get('guideline-auditor')!)const geneticsAudit = requireStructured<GeneticsAudit>('genetics-auditor', auditMap.get('genetics-auditor')!)const webClaimsAudit = requireStructured<WebClaimsAudit>('web-claims-auditor', auditMap.get('web-claims-auditor')!)const safetyAudit = requireStructured<SafetyAudit>('safety-boundary-agent', auditMap.get('safety-boundary-agent')!)console.log('\n[Phase 1 Summary]')for (const run of auditRuns) {const status = run.result.success ? 'OK' : 'FAILED'console.log(` ${run.name.padEnd(30)} [${status}] ${Math.round(run.elapsedMs)}ms, ${run.result.tokenUsage.output_tokens} out tokens`)}console.log(` Audit wall time: ${Math.round(auditElapsed)}ms\n`)console.log('[Phase 2] Downstream arbitration over structured audit outputs only\n')const arbiterPrompt = `You are given structured outputs from six source-isolated audits.You cannot access the original fixtures. Decide whether this case is a credible lead,conflicting evidence, misleading/commercial, or needs specialist review.SYMPTOM AUDIT:${JSON.stringify(symptomAudit, null, 2)}NONPROFIT EDUCATION AUDIT:${JSON.stringify(nonprofitAudit, null, 2)}OFFICIAL GUIDELINE / EXPERT-CONSENSUS AUDIT:${JSON.stringify(guidelineAudit, null, 2)}GENE-PHENOTYPE AUDIT:${JSON.stringify(geneticsAudit, null, 2)}WEB / FORUM / COMMERCIAL CLAIMS AUDIT:${JSON.stringify(webClaimsAudit, null, 2)}SAFETY BOUNDARY AUDIT:${JSON.stringify(safetyAudit, null, 2)}Rules:- Do not produce a diagnosis.- Do not recommend a treatment, dose, paid test, or commercial product.- Surface the conflict between over-specific web/commercial claims and broader official/gene evidence.- Return one JSON object matching the schema, no markdown.`const arbiterStart = performance.now()const arbiterResult = await triageArbiter.run(arbiterPrompt)const arbiterElapsed = performance.now() - arbiterStartconst decision = requireStructured<TriageDecision>('rare-disease-triage-arbiter', arbiterResult)console.log(` Arbiter [OK] ${Math.round(arbiterElapsed)}ms, ${arbiterResult.tokenUsage.output_tokens} out tokens\n`)// ---------------------------------------------------------------------------// Runtime assertions// ---------------------------------------------------------------------------const allowedConflictDecisions: TriageDecision['decision'][] = ['conflicting_evidence','misleading_or_commercial','needs_specialist_review',]const asserts = [{name: 'decision should reflect conflict/safety concern',pass: allowedConflictDecisions.includes(decision.decision),},{name: 'patient-facing diagnosis should not be allowed',pass: decision.patient_facing_answer_allowed === false,},{name: 'diagnosis should not be provided',pass: decision.diagnosis_provided === false,},{name: 'conflicts should be surfaced',pass: decision.conflicts.length > 0,},{name: 'unsafe elements should be surfaced',pass: decision.unsafe_elements.length > 0,},]console.log('='.repeat(88))console.log('RARE DISEASE INFORMATION TRIAGE DECISION')console.log('='.repeat(88))console.log(JSON.stringify(decision, null, 2))console.log()console.log('## Runtime Assertions\n')let hasFailure = falsefor (const assertion of asserts) {console.log(`- ${assertion.pass ? 'PASS' : 'FAIL'}: ${assertion.name}`)if (!assertion.pass) hasFailure = true}const totalOutputTokens =auditRuns.reduce((sum, run) => sum + run.result.tokenUsage.output_tokens, 0) +arbiterResult.tokenUsage.output_tokensconst totalInputTokens =auditRuns.reduce((sum, run) => sum + run.result.tokenUsage.input_tokens, 0) +arbiterResult.tokenUsage.input_tokensconsole.log('\n## Token Usage\n')console.log(`Input tokens: ${totalInputTokens}`)console.log(`Output tokens: ${totalOutputTokens}`)console.log('Estimated cost depends on the selected provider/model; this example prints token usage for transparency.')console.log('\n' + '='.repeat(88))if (hasFailure) {console.error('Runtime assertion failed.')process.exit(1)}console.log('Rare disease information triage example complete.\n')// ---------------------------------------------------------------------------// Production source adapter sketch// ---------------------------------------------------------------------------//// This example intentionally uses committed MOCK fixtures so the demo is// deterministic, safe, and runnable. A production version could replace fixtures// with source adapters for patient intake forms, patient-advocacy education,// official guideline repositories, gene-phenotype databases, web-claim crawlers,// and an internally approved medical safety policy service. The source-isolated// audit + downstream arbitration shape would remain the same.
open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。