从这则新闻说起

2026年5月,《黑袍纠察队》主演Erin Moriarty公开了自己与Graves病抗争的经历。她在拍摄最终季时出现心悸、手抖、体重骤降等症状,但在确诊前经历了数月的困惑和误诊——这几乎是所有罕见/自身免疫病患者共同的困境:症状不典型,医生无法立即判断,患者自己又缺乏可靠的信息渠道。

作为开发者,我读到这篇的第一反应是:我们能不能做一个工具,让用户输入症状,立刻得到基于权威医学指南的分析和建议?

这不是在教唆自诊,而是帮患者在就医前理清思路、准备问题、加速准确诊断。Graves病的误诊率高达30%(根据美国甲状腺协会数据),如果AI能将这个比例降低哪怕几个百分点,价值巨大。

本文我会直接给你一个可运行的项目原型:一个基于RAG(检索增强生成)的在线症状查询助手。你可以在本地跑起来,或者一键部署到Vercel。

效果演示

用户输入症状描述(如“心悸、手抖、容易累、体重下降”),系统会:

  1. 从预先索引的医学知识库(梅奥诊所、NHS、中国甲状腺疾病诊治指南等)中检索最相关的段落
  2. 用LLM生成一份结构化的分析报告,包括:可能的疾病方向、建议就诊科室、需要做的检查、需要向医生提出的问题
  3. 明确附上免责声明和就医建议

症状查询页面截图

技术选型及理由

模块 技术 原因
前端框架 Next.js 15 App Router SSG + 流式渲染,适合部署到Vercel,零运维
向量数据库 内存中的 @xata-io/pgvector 或者 chromadb 客户端 小规模演示不需要额外服务,内存即可
Embedding OpenAI text-embedding-3-small 1536维,性价比高,单次embedding成本约0.00002美元
LLM OpenAI GPT-4o-mini 成本低(百万token约0.15美元),流式输出体验好
知识库 手动整理的Markdown文件(来自公开医学指南) 可控、无版权风险,且可以按需定制

为什么不直接用GPT-4本身? 纯LLM回答医疗问题容易出现幻觉,而且没有来源引用。RAG确保每个回答都锚定在可信文本上,这在医疗场景是底线。

核心代码实现

1. 构建知识库索引

将医学指南分割成chunks,生成embedding并存储。我用了一个简单的JSON文件作为向量存储(生产环境请用pgvector或Qdrant)。

typescript
1 2 3 4 5 6 7 8 9 10 11 12
// lib/embeddings.ts
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function getEmbedding(text: string): Promise<number[]> {
  const res = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });
  return res.data[0].embedding;
}
typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// lib/indexer.ts
import { getEmbedding } from './embeddings';
import fs from 'fs/promises';

interface Chunk {
  text: string;
  source: string;
  embedding: number[];
}

const CHUNK_SIZE = 500; // tokens

export async function indexDocument(filePath: string) {
  const content = await fs.readFile(filePath, 'utf-8');
  // 简单按段落分割,更精确可用langchain文本分割器
  const paragraphs = content.split(/\n\n+/).filter(p => p.trim().length > 50);
  const chunks: Chunk[] = [];
  for (const para of paragraphs) {
    const embedding = await getEmbedding(para);
    chunks.push({ text: para, source: filePath, embedding });
  }
  await fs.writeFile('data/index.json', JSON.stringify(chunks, null, 2));
  console.log(`Indexed ${chunks.length} chunks.`);
}

实际运行时,我预先索引了以下来源:

  • 梅奥诊所 Graves 病页面
  • NHS 甲状腺功能亢进指南
  • 中国《甲状腺功能亢进症诊治指南》(2022)
  • 美国甲状腺协会患者教育材料

2. 查询时检索

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// lib/search.ts
import { getEmbedding } from './embeddings';
import indexData from '@/data/index.json';

// 余弦相似度
function cosineSimilarity(a: number[], b: number[]): number {
  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

export async function searchSimilar(query: string, topK = 5) {
  const queryEmbedding = await getEmbedding(query);
  const scored = indexData.map(chunk => ({
    ...chunk,
    score: cosineSimilarity(queryEmbedding, chunk.embedding),
  }));
  scored.sort((a, b) => b.score - a.score);
  return scored.slice(0, topK);
}

3. 生成最终回答(流式输出)

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// app/api/analyze/route.ts
import { searchSimilar } from '@/lib/search';
import OpenAI from 'openai';

export async function POST(req: Request) {
  const { symptoms } = await req.json();
  if (!symptoms || typeof symptoms !== 'string') {
    return new Response('Missing symptoms', { status: 400 });
  }

  const relevantChunks = await searchSimilar(symptoms);
  const context = relevantChunks.map(c => c.text).join('\n\n---\n\n');

  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

  const systemPrompt = `你是一个基于权威医学指南的辅助分析工具。
用户会描述他们的症状。你将从以下知识库中检索到的信息出发,提供:
1. 可能的疾病方向(明确说明这只是可能性,不是诊断)
2. 建议就诊科室
3. 可以做的检查
4. 需要向医生提出的关键问题

始终包含免责声明:"我是AI,不能替代医生诊断。请及时就医。"

知识库上下文:
${context}`;

  const stream = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: `我的症状是:${symptoms}` },
    ],
    stream: true,
  });

  return new Response(
    new ReadableStream({
      async start(controller) {
        for await (const chunk of stream) {
          const content = chunk.choices[0]?.delta?.content || '';
          if (content) controller.enqueue(new TextEncoder().encode(content));
        }
        controller.close();
      },
    }),
    { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }
  );
}

前端直接用fetch + ReadableStream渲染,代码就不贴了,GitHub仓库里有完整的React Server Components实现。

项目结构

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
symptom-analyzer/
├── app/
│   ├── page.tsx          # 主页面(输入框)
│   ├── result/           # 结果页(流式渲染)
│   └── api/analyze/route.ts
├── lib/
│   ├── embeddings.ts
│   ├── indexer.ts
│   └── search.ts
├── data/
│   ├── index.json        # 预生成的向量索引(约5MB)
│   └── sources/          # 原始Markdown文件
├── public/
├── next.config.js
├── package.json
└── vercel.json

部署到Vercel时,直接将 data/index.json 作为静态资源保留下。由于向量索引是本地JSON,不需要数据库,所以冷启动也很快(约200ms)。

上线要踩的坑

1. 免责声明必须显眼,且不可被用户跳过

我参考了Google Health的实践:在结果顶部用红色警示框显示“此工具不能替代医生诊断”。同时API返回的文本开头强制插入免责声明(即使LLM有时会忘记)。

2. 医疗知识库的时效性

Graves病的诊疗指南每年都有小更新。建议设置一个 lastUpdated 字段,同时在UI上显示“知识库更新时间:2026年4月”。当知识库太旧时(比如超过1年),主动提示用户信息可能过时。

3. Embedding + 流式输出的组合问题

OpenAI的流式输出分块可能包含不完整的标记。如果前端直接显示,部分内容可能被截断。我的做法是:不在前端做额外处理,让 ReadableStream 直接输出文本片段,浏览器会自然渲染。但如果想高亮引用的来源,则需要后端先组装完整句子再返回,但这会牺牲实时性。我选择牺牲实时性——医疗场景准确性优先。

4. 并发与速率限制

免费OpenAI账号有每分钟500次embedding的限制,而每个查询需要一次embedding + 一次LLM调用。如果一天几千次查询,embedding容易打满。我的方案:对常见症状组合(如“心悸手抖”)做embedding缓存,用Redis或Vercel KV存储。命中率约30%,能降低约三分之一的embedding调用。

5. 不要返回“确诊”结论

哪怕知识库说“Graves病最常见的症状包括……”,LLM也可能输出“您可能患有Graves病”。我在system prompt里加了一条强约束:只能使用“可能与……相关”“建议排查”等表述,严禁出现“您患有”字样。并用few-shot示例强化。

这工具真的能帮到患者吗?

Erin Moriarty 的故事里有一个细节:她对着镜子看到自己眼球突出,但直到内分泌科医生发邮件说“我们有答案了”才真正知道是Graves病。如果她早几个月用这个工具,输入“眼突+心悸+体重下降”,AI会立刻检索到Graves病是高度相关的可能,并建议她看内分泌科做TSH受体抗体检测。这可能会缩短确诊周期——当然,前提是她能及时就医。

我不是在宣称AI能解决一切,但把权威信息以个人化、易理解的方式呈现,本身就是一种赋权。开发者应该关注的是:如何让LLM在约束下提供可靠信息,而不是让它自由发挥。 本文的代码虽然简单,但核心机制(检索+约束生成)是任何专业领域AI助手的基础。

你可以把这个模板套用到法律咨询、营养建议、宠物健康——工程模式是一样的,变化的是知识库和约束规则。祝你的第一个AI医疗助手早日上线。


所有代码可在 [github.com/yeqingyuan/symptom-analyzer] 获取。