AI가 프로테오믹스 리포트를 쓸 수 있을까 — Ollama 환각과의 전쟁
DE 분석 결과를 LLM이 해석하여 리포트를 자동 생성하려 했다. Ollama qwen3:30b의 환각 — 없는 단백질 생성, 수치 날조, 가짜 논문 인용. anti-hallucination 프롬프트와 템플릿 기반 렌더링으로 환각 0%를 달성한 과정.
목표 — AI가 분석 결과를 해석해준다면
BioAI Market에서 DE 분석이 끝나면 수백~수천 개의 단백질에 대한 log2FC, p-value, adj.p-value가 나온다. 연구자가 이 숫자들을 보고 의미를 해석하는 건 시간이 걸린다. "AI가 이걸 자동으로 해석해서 리포트를 써주면 어떨까?" — 자연스러운 발상이었다.
1차 시도 — 결과를 그냥 던져주기
Ollama의 qwen3:30b에 DE 결과를 넣고 해석을 요청했다.
const prompt = `
다음은 프로테오믹스 DE 분석 결과입니다. 주요 발견사항을 해석해주세요.
Upregulated (adj.p < 0.05, log2FC > 1):
- HSPA5: log2FC=2.41, adj.p=0.0012
- GRP78: log2FC=1.89, adj.p=0.0034
- CALR: log2FC=1.56, adj.p=0.0089
Downregulated (adj.p < 0.05, log2FC < -1):
- COL1A1: log2FC=-2.13, adj.p=0.0005
- FN1: log2FC=-1.78, adj.p=0.0021
`
const response = await ollamaGenerate({
model: 'qwen3:30b',
prompt,
system: 'You are a proteomics expert.'
})
결과:
## 분석 결과 해석
본 분석에서 HSPA5 (log2FC=2.41)와 GRP78 (log2FC=1.89)의 유의미한 상향조절이 관찰되었습니다.
특히 **BRCA1** (log2FC=1.23, adj.p=0.015)의 상향조절은 DNA 손상 반응 경로의 활성화를 시사하며,
이는 Kim et al. (2024, Journal of Proteome Research)의 유방암 연구와 일치합니다.
또한 **TP53** 경로의 하향조절과 함께 **VEGFA** (log2FC=-1.45)의 감소는
항혈관신생 효과를 나타내며...
문제점:
- BRCA1 — 입력 데이터에 없다. 날조.
- log2FC=1.23 — 완전히 만들어낸 수치.
- Kim et al. (2024) — 존재하지 않는 논문.
- TP53, VEGFA — 데이터에 없는 단백질.
LLM이 "그럴듯한 과학적 서술"을 만들어내는 데는 탁월했지만, 사실에 근거하지 않은 내용이 절반 이상이었다. 이걸 그대로 리포트에 넣으면? 연구자가 잘못된 결론을 내릴 수 있다. 과학 데이터에서 환각은 단순 불편이 아니라 위험이다.
환각 유형 분류
한 달간 수집한 환각 패턴을 분류했다:
1. 없는 단백질 생성 (가장 흔함)
데이터에 5개 단백질만 있는데 10개를 언급한다. 나머지 5개는 LLM이 "알고 있는" 관련 단백질을 그럴듯하게 끼워넣는 것이다.
2. 수치 변조
log2FC=2.41을 "약 2.5"로 반올림하거나, 아예 다른 숫자를 적는다. 소수점 이하가 미묘하게 다른 경우가 많아서 눈으로 확인하지 않으면 모를 수 있다.
3. 가짜 논문 인용
"Smith et al. (2023, Nature)", "Lee et al. (2024, Cell)" — 저자, 연도, 저널이 조합된 가짜 레퍼런스를 자신있게 인용한다. DOI까지 만들어내는 경우도 있다.
4. 근거 없는 질병 연관성
"이 패턴은 알츠하이머 초기 단계와 관련이 있다"처럼, 바이오마커 DB에도 없는 연관성을 주장한다.
Anti-Hallucination 프롬프트 — 6가지 절대 규칙
프롬프트 엔지니어링으로 환각을 줄이려 했다:
const ANTI_HALLUCINATION_SYSTEM = `You are a proteomics data analyst. You MUST follow these 6 absolute rules:
RULE 1: ONLY mention proteins that appear in the PROVIDED DATA below.
If a protein is not in the data, you MUST NOT mention it. Period.
RULE 2: ONLY quote numerical values EXACTLY as they appear in the data.
Never round, estimate, approximate, or fabricate any number.
Write "log2FC=2.41" not "log2FC≈2.4" or "about 2.5".
RULE 3: NEVER cite specific papers, authors, journals, or DOIs.
Instead use phrases like "literature suggests" or "previous studies indicate".
If you cannot make a claim without a citation, do not make the claim.
RULE 4: NEVER claim biomarker-disease associations unless they are
EXPLICITLY provided in the input data section labeled "KNOWN ASSOCIATIONS".
RULE 5: If you are unsure or the data is insufficient, say:
"The data does not provide sufficient evidence to conclude..."
NEVER guess or fill gaps with general knowledge.
RULE 6: Every factual claim must be directly traceable to the input data.
Before writing each sentence, verify: "Is this in the data?"
If NO → do not write it.`
결과: 70% 개선, 하지만 불완전
이 프롬프트를 적용하니 대부분의 환각이 사라졌다:
- 가짜 논문 인용 → 거의 0%
- 없는 단백질 생성 → 크게 감소
- 수치 변조 → 여전히 가끔 발생 (5~10%)
- 근거 없는 연관성 → 여전히 가끔 발생
"가끔"이 문제다. 100번 중 5번이 환각이면, 리포트를 매번 사람이 검증해야 한다. 그러면 자동 생성의 의미가 없다.
최종 해결 — 템플릿 기반 렌더링
결론은 LLM에게 리포트를 "쓰게" 하지 말고, "채우게" 하는 것이었다.
result-templates.ts — 구조화된 템플릿
// result-templates.ts
export function renderDEReport(deResult: DEResult): string {
const { upregulated, downregulated, summary } = deResult
let report = `## Differential Expression Analysis Results\n\n`
// 수치/테이블은 템플릿이 직접 생성 — 환각 불가능
report += `### Summary Statistics\n`
report += `- Total proteins analyzed: **${summary.total}**\n`
report += `- Significant (adj.p < 0.05): **${summary.significant}** `
report += `(${((summary.significant / summary.total) * 100).toFixed(1)}%)\n`
report += `- Upregulated (log2FC > 1): **${upregulated.length}**\n`
report += `- Downregulated (log2FC < -1): **${downregulated.length}**\n\n`
// Top upregulated 테이블
report += `### Top Upregulated Proteins\n\n`
report += `| Protein | log2FC | adj.p-value |\n`
report += `|---------|--------|-------------|\n`
for (const p of upregulated.slice(0, 10)) {
report += `| ${p.protein} | ${p.log2fc.toFixed(3)} | ${p.adjPvalue.toExponential(2)} |\n`
}
// Top downregulated 테이블
report += `\n### Top Downregulated Proteins\n\n`
report += `| Protein | log2FC | adj.p-value |\n`
report += `|---------|--------|-------------|\n`
for (const p of downregulated.slice(0, 10)) {
report += `| ${p.protein} | ${p.log2fc.toFixed(3)} | ${p.adjPvalue.toExponential(2)} |\n`
}
return report
}
LLM은 "해석" 부분만
export async function generateInterpretation(
deResult: DEResult,
pathwayResult: PathwayResult
): Promise<string> {
// 템플릿으로 수치/테이블 생성 (환각 0%)
const dataSection = renderDEReport(deResult)
const pathwaySection = renderPathwayReport(pathwayResult)
// LLM은 해석만 담당 (수치 인용 금지)
const interpretation = await ollamaGenerate({
model: 'qwen3:30b',
system: ANTI_HALLUCINATION_SYSTEM,
prompt: `Based on the following analysis results, provide a brief biological interpretation.
DO NOT repeat any numbers or protein names. Focus on biological meaning and potential implications.
${dataSection}
${pathwaySection}
Write 3-5 sentences of interpretation only.`
})
// 최종 리포트 = 템플릿(수치) + LLM(해석)
return `${dataSection}\n${pathwaySection}\n\n### Interpretation\n\n${interpretation}`
}
이 구조에서:
- 수치, 테이블, 단백질 이름 → 템플릿이 data에서 직접 렌더링. 환각 불가능.
- 생물학적 해석 → LLM이 작성. 하지만 수치를 인용하지 않으니 변조 위험 없음.
**환각률: 실질적으로 0%**가 됐다.
DEP-Biomarker 검증 — DB 데이터만 표시
DE 결과에서 나온 단백질이 알려진 바이오마커인지 확인하는 기능도 만들었다. 여기서도 LLM을 쓰지 않고 DB 매칭으로 처리했다:
async function matchBiomarkers(deProteins: string[]): Promise<BiomarkerMatch[]> {
const { data: matches } = await supabase
.from('biomarkers')
.select('name, disease, evidence_level, source')
.in('gene_symbol', deProteins)
// DB에 있는 것만 반환 — LLM이 끼어들 여지 없음
return matches.map(m => ({
protein: m.name,
disease: m.disease,
evidenceLevel: m.evidence_level,
source: m.source // PubMed ID 등 실제 출처
}))
}
DB에 없으면 "연관된 바이오마커가 발견되지 않았습니다"라고 표시. 절대로 추측하지 않는다.
교훈 — LLM에게 "쓰게" 하지 말고 "채우게" 하라
이 프로젝트에서 얻은 가장 큰 교훈:
- 과학 데이터에서 LLM 환각은 위험하다 — 수치 하나가 잘못되면 연구 결론이 바뀔 수 있다
- 프롬프트만으로는 환각을 0%로 만들 수 없다 — 70%는 줄여도 나머지 30%가 문제
- 템플릿 + LLM 하이브리드가 정답 — 수치/사실은 코드가, 해석은 LLM이
- DB 매칭 > LLM 추론 — 바이오마커-질병 연관성 같은 사실관계는 DB에서 직접 조회
- LLM의 역할을 제한하라 — "리포트를 써라"가 아니라 "이 데이터가 생물학적으로 무엇을 의미하는지 3문장으로 설명해라"
결국 LLM은 자유작문 도구가 아니라 빈칸 채우기 도구로 써야 안전하다. 특히 과학/의료 도메인에서는.
❌ "이 데이터를 분석하고 리포트를 작성해줘"
✅ "아래 3개 문장의 빈칸을 채워줘. 데이터에 없는 내용은 '불충분'이라고 써"
이 원칙을 적용한 뒤로 환각 관련 이슈는 완전히 사라졌다.
참고 링크: