| name | collect |
| description | ⚠️ DEPRECATED — 이 스킬은 더 이상 사용하지 않는다. 수집/스크리닝/요약/번역은 기존 API 스크립트(collect-papers.ts, collect-community.ts, summarize.ts, digest-community.ts, translate.ts)를 직접 실행한다. 아래 내용은 레거시 참고용으로만 보존. |
⚠️ DEPRECATED: 이 스킬은 레거시입니다. 아래 원래 API 기반 파이프라인을 사용하세요.
원래 API 파이프라인 (현재 사용)
source .env
npx tsx scripts/collect-papers.ts
npx tsx scripts/collect-community.ts
npx tsx scripts/summarize.ts
npx tsx scripts/digest-community.ts
npx tsx scripts/translate.ts
레거시 내용 (참고용)
collect 파이프라인
Claude Code가 스크리닝/요약/번역을 직접 수행한다. API 호출(screenBatch, summarize 스크립트) 절대 사용 금지.
⚠️ collect-papers.ts는 ANTHROPIC_API_KEY 없어도 screenBatch를 항상 실행하여 score=0으로 캐시 저장 후 0개 저장한다. 이 스크립트를 직접 실행하면 안 된다.
전제 조건
- 작업 디렉토리:
/Users/ram/programming/vibecoding/ai-paper
.env에 TURSO_DATABASE_URL, TURSO_AUTH_TOKEN 있어야 함
ANTHROPIC_API_KEY 사용 금지 (스크리닝/요약/번역은 Claude가 직접)
실행 순서
1. 수집 (스크리닝 없이 전체 저장)
아래 스크립트를 /tmp/collect-no-screen.ts에 생성 후 실행:
cat > /tmp/collect-no-screen.ts << 'EOF'
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from '/Users/ram/programming/vibecoding/ai-paper/src/lib/db/schema';
import { eq } from 'drizzle-orm';
const client = createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN! });
const db = drizzle(client, { schema });
const HF_API = 'https://huggingface.co/api/papers?limit=40';
async function main() {
const { fetchRecentPapers } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/arxiv/client');
const { calculateHotScore } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/hot-scorer');
console.log('📄 arXiv 수집...');
const arxivPapers = await fetchRecentPapers(100);
let arxivSaved = 0;
for (const p of arxivPapers) {
const exists = await db.select().from(schema.papers).where(eq(schema.papers.id, p.id)).limit(1);
if (exists.length > 0) continue;
const hotScore = calculateHotScore(p);
await db.insert(schema.papers).values({ ...p, authors: JSON.stringify(p.authors), categories: JSON.stringify(p.categories), hotScore, isHot: hotScore >= 70, collectedAt: new Date().toISOString() }).onConflictDoNothing();
arxivSaved++;
}
console.log(`arXiv: ${arxivSaved}개 신규 저장`);
console.log('📄 HuggingFace 수집...');
const hfRes = await fetch(HF_API);
const hfPapers: any[] = await hfRes.json();
let hfSaved = 0;
for (const p of hfPapers) {
const exists = await db.select().from(schema.papers).where(eq(schema.papers.id, p.id)).limit(1);
if (exists.length > 0) continue;
await db.insert(schema.papers).values({ id: p.id, title: p.title, abstract: p.summary ?? '', authors: JSON.stringify((p.authors ?? []).map((a: any) => a.name)), categories: JSON.stringify([]), primaryCategory: 'cs.AI', publishedAt: p.publishedAt, arxivUrl: `https://arxiv.org/abs/${p.id}`, pdfUrl: `https://arxiv.org/pdf/${p.id}`, source: 'hugging_face', hotScore: 50, isHot: false, collectedAt: new Date().toISOString() }).onConflictDoNothing();
hfSaved++;
}
console.log(`HuggingFace: ${hfSaved}개 신규 저장`);
}
main().catch(console.error);
EOF
export $(grep -E '^TURSO_' .env | xargs) && NODE_PATH=/Users/ram/programming/vibecoding/ai-paper/node_modules npx tsx /tmp/collect-no-screen.ts
커뮤니티(HN/Reddit) 수집 — 마찬가지로 no-screen 버전 사용:
cat > /tmp/collect-community-no-screen.ts << 'EOF'
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from '/Users/ram/programming/vibecoding/ai-paper/src/lib/db/schema';
import { eq } from 'drizzle-orm';
const client = createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN! });
const db = drizzle(client, { schema });
async function main() {
const { fetchHNTopAI } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/hacker-news/client');
const { fetchRedditAI } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/reddit/client');
console.log('📰 HN 수집...');
const stories = await fetchHNTopAI(100);
let hnSaved = 0;
for (const s of stories) {
const id = 'hn_' + s.id;
const exists = await db.select().from(schema.papers).where(eq(schema.papers.id, id)).limit(1);
if (exists.length > 0) continue;
const hotScore = Math.min(Math.floor(s.score / 5), 100);
await db.insert(schema.papers).values({
id, title: s.title, abstract: s.title,
authors: JSON.stringify([s.by]), categories: JSON.stringify(['hacker_news']),
primaryCategory: 'hacker_news',
publishedAt: new Date(s.time * 1000).toISOString(),
arxivUrl: s.url ?? 'https://news.ycombinator.com/item?id=' + s.id,
pdfUrl: '', source: 'hacker_news', hotScore, isHot: hotScore >= 70,
collectedAt: new Date().toISOString(),
}).onConflictDoNothing();
hnSaved++;
}
console.log('HN:', hnSaved, '개 저장');
console.log('🔴 Reddit 수집...');
const posts = await fetchRedditAI();
let redditSaved = 0;
for (const p of posts) {
const id = 'reddit_' + p.subreddit + '_' + p.id;
const exists = await db.select().from(schema.papers).where(eq(schema.papers.id, id)).limit(1);
if (exists.length > 0) continue;
await db.insert(schema.papers).values({
id, title: p.title, abstract: p.selftext || p.title,
authors: JSON.stringify([p.author]), categories: JSON.stringify(['reddit_' + p.subreddit]),
primaryCategory: 'reddit_' + p.subreddit,
publishedAt: new Date(p.created_utc * 1000).toISOString(),
arxivUrl: p.url || 'https://www.reddit.com' + p.permalink,
pdfUrl: '', source: 'reddit', hotScore: 50, isHot: false,
collectedAt: new Date().toISOString(),
}).onConflictDoNothing();
redditSaved++;
}
console.log('Reddit:', redditSaved, '개 저장');
}
main().catch(console.error);
EOF
export $(grep -E '^TURSO_' .env | xargs) && NODE_PATH=/Users/ram/programming/vibecoding/ai-paper/node_modules npx tsx /tmp/collect-community-no-screen.ts
2. 수집된 논문 확인
export $(grep -E '^TURSO_' .env | xargs) && NODE_PATH=/Users/ram/programming/vibecoding/ai-paper/node_modules npx tsx -e "
(async () => {
const { createClient } = await import('@libsql/client');
const client = createClient({ url: process.env.TURSO_DATABASE_URL, authToken: process.env.TURSO_AUTH_TOKEN });
const r = await client.execute('SELECT id, title, source, abstract FROM papers WHERE summarized_at IS NULL ORDER BY collected_at DESC');
for (const row of r.rows) console.log('['+row.source+'] '+row.id+': '+String(row.title).slice(0,80));
console.log('총:', r.rows.length, '개');
})();"
3. 스크리닝 (Claude가 직접)
수집된 논문 제목/내용을 보고 AI 개발자에게 유용하지 않은 것 삭제.
PASS 기준: AI 개발자가 즉시 적용 가능한 프롬프팅, 에이전트, 보안, 워크플로우 관련
REJECT 기준: 순수 수학, 모델 학습 이론, 의료/법률 도메인, 뉴스/공지만 있는 것
스크리닝 후 탈락 논문 DB에서 삭제:
export $(grep -E '^TURSO_' .env | xargs) && NODE_PATH=/Users/ram/programming/vibecoding/ai-paper/node_modules npx tsx -e "
(async () => {
const { createClient } = await import('@libsql/client');
const client = createClient({ url: process.env.TURSO_DATABASE_URL, authToken: process.env.TURSO_AUTH_TOKEN });
const ids = ['id1', 'id2']; // 탈락 ID 목록
for (const id of ids) {
await client.execute({ sql: 'DELETE FROM papers WHERE id = ?', args: [id] });
}
console.log('삭제 완료:', ids.length, '개');
})();"
4. 요약 — 논문 vs 커뮤니티 분리 처리
논문(arxiv/hugging_face)과 커뮤니티(hacker_news/reddit)는 처리 방식이 다르다.
4-A. 커뮤니티 요약 (Claude가 직접)
커뮤니티 아이템은 digest-community.ts를 사용하면 안 됨 (runClaude() = API 호출).
원문 품질 보장이 핵심 — 이 단계를 건너뛰면 나중에 수동 재검증이 필요해진다.
Claude가 직접 각 아이템에 대해:
- 원문 fetch:
src/lib/content-fetcher.ts의 fetchContent(arxivUrl) 실행
- 원문 품질 체크 (필수):
- 원문이 500자 미만이거나 네비게이션/보일러플레이트만 있으면 → 즉시 WebFetch 재시도
WebFetch(url, "Extract all technical details: what it does, how it works, key results, specific numbers") 호출
- WebFetch도 실패하거나 내용 없으면 → WebSearch로 논문/블로그 검색
- Google Research·Anthropic·OpenAI 블로그·Reddit 포스트는 fetchContent 실패 확률 높음 — 처음부터 WebFetch 우선 고려
- 댓글 fetch:
- HN:
fetchHNComments(hnId, 15) (공개 API)
- Reddit:
fetchRedditComments(url, 15) (공개 API)
- 원문 + 댓글 전체 읽고 요약 작성 — 요약 전 반드시 충분한 기술 내용이 확보됐는지 확인
- DB UPDATE
이 단계를 제대로 하면 별도 품질 검증 불필요.
커뮤니티 아이템 조회:
export $(grep -E '^TURSO_' .env | xargs) && NODE_PATH=/Users/ram/programming/vibecoding/ai-paper/node_modules npx tsx -e "
(async () => {
const { createClient } = await import('@libsql/client');
const client = createClient({ url: process.env.TURSO_DATABASE_URL, authToken: process.env.TURSO_AUTH_TOKEN });
const r = await client.execute(\"SELECT id, title, source, arxiv_url FROM papers WHERE summarized_at IS NULL AND source IN ('hacker_news','reddit') ORDER BY collected_at DESC\");
for (const row of r.rows) console.log('['+row.source+'] '+row.id+': '+String(row.title).slice(0,80));
console.log('총:', r.rows.length, '개');
})();"
원문+댓글 fetch 예시:
const { fetchContent } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/content-fetcher');
const { fetchHNComments } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/hacker-news/client');
const content = await fetchContent(arxivUrl);
const hnId = parseInt(id.replace('hn_', ''));
const comments = await fetchHNComments(hnId, 15);
const { fetchRedditComments } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/reddit/client');
const content = await fetchContent(arxivUrl);
const comments = await fetchRedditComments(arxivUrl, 15);
4-B. 논문 요약 (에이전트 병렬)
미요약 논문 ID를 전부 조회 후 10개 배치로 나눠 Agent 10개 병렬 스폰.
각 에이전트는 자신의 배치 ID만 처리 (중복 없음).
⚠️ 필수: 초록(abstract)만 읽으면 안 됨. 반드시 PDF 전문을 다운로드해서 읽고 요약할 것.
각 에이전트의 논문별 처리 순서:
- DB에서
pdfUrl 조회
/tmp/pdf-{id}.ts 스크립트로 PDF 전문 다운로드 후 /tmp/pdf-{id}.txt에 저장
- 전문을 모두 읽고 요약 작성 (아래 주의사항 필수)
- DB UPDATE
⚠️ PDF 전문 읽기 주의사항 (반드시 준수)
- Read 도구는 한 번에 10,000 토큰(약 400줄) 제한이 있음
- 논문은 보통 800~2000줄 → 반드시 여러 번 Read해서 전체를 읽어야 함
- 읽는 방법:
offset=1, limit=400 → offset=401, limit=400 → offset=801, limit=400 → 끝날 때까지 반복
- 먼저 파일의 총 줄 수를 파악한 후, 모든 섹션(서론, 방법론, 실험, 결과, 결론)을 빠짐없이 읽을 것
- 앞뒤만 읽고 요약하는 것은 절대 금지. 중간 실험/결과 섹션을 빠뜨리면 요약 품질이 심각하게 저하됨
PDF 저장 스크립트 예시:
const { fetchPdfText } = await import('/Users/ram/programming/vibecoding/ai-paper/src/lib/pdf-fetcher');
const fs = await import('fs');
const text = await fetchPdfText(pdfUrl);
fs.writeFileSync('/tmp/pdf-{id}.txt', text);
console.log('줄 수:', text.split('\n').length, '/ 글자수:', text.length);
실행: NODE_PATH=/Users/ram/programming/vibecoding/ai-paper/node_modules npx tsx /tmp/fetch-pdf-{id}.ts
읽기 반복 예시 (1200줄 논문의 경우):
- Read(
/tmp/pdf-{id}.txt, offset=1, limit=400) → 서론/배경
- Read(
/tmp/pdf-{id}.txt, offset=401, limit=400) → 방법론
- Read(
/tmp/pdf-{id}.txt, offset=801, limit=400) → 결과/결론
→ 모두 읽은 후에만 요약 작성
병렬 스폰 방법:
SELECT id, title, pdf_url FROM papers WHERE summarized_at IS NULL ORDER BY id 로 미요약 논문 전체 조회
Math.ceil(total / 10) 씩 10개 배치 분할
- Agent 10개 동시 스폰 (run_in_background: true)
- 각 에이전트 프롬프트에 담당 ID 배열 + pdfUrl 명시
PDF fetch 예시 (각 에이전트가 /tmp 스크립트로 실행):
import { fetchPdfText } from '/Users/ram/programming/vibecoding/ai-paper/src/lib/pdf-fetcher';
const fullText = await fetchPdfText(pdfUrl);
요약 필드:
title_ko: 한국어 제목
one_liner: 한 줄 요약 (한국어)
target_audience: 대상 독자 (한국어)
key_findings: JSON array (한국어, 3-5개)
evidence: JSON array (한국어, 2-3개)
how_to_apply: JSON array (한국어, 2-3개)
glossary: JSON object 형식 {"용어": "설명", ...} (한국어, 2-4개) — ⚠️ 절대 문자열 배열 사용 금지 (페이지 렌더링 깨짐)
tags: JSON array - 허용값: ["에이전트","프롬프트엔지니어링","RAG","파인튜닝","코드생성","멀티모달","평가","벤치마크","보안","프롬프트인젝션","도구사용","함수호출","추론최적화","양자화","캐싱","레드팀","프라이버시","임베딩","벡터검색","청킹","MCP","LoRA","RLHF"]
ai_category: "prompting"|"rag"|"agent"|"fine-tuning"|"eval"|"security"|"cost-speed"
dev_relevance: 1-5 (개발자 관련도)
summarized_at: new Date().toISOString()
UPDATE 쿼리:
UPDATE papers SET title_ko=?, one_liner=?, target_audience=?, key_findings=?, evidence=?, how_to_apply=?, glossary=?, tags=?, ai_category=?, dev_relevance=?, summarized_at=? WHERE id=?
5. 번역 (에이전트 병렬)
요약 완료 후 동일하게 10개 배치로 나눠 Agent 10개 병렬 스폰.
SELECT id FROM papers WHERE summarized_at IS NOT NULL AND one_liner_en IS NULL ORDER BY id 로 미번역 조회.
영어 번역 필드:
one_liner_en, target_audience_en, key_findings_en, evidence_en, how_to_apply_en, glossary_en, tags_en
Tags 매핑: 에이전트→Agent, 프롬프트엔지니어링→Prompt Engineering, RAG→RAG, 파인튜닝→Fine-tuning, 코드생성→Code Generation, 멀티모달→Multimodal, 평가→Evaluation, 벤치마크→Benchmark, 보안→Security, 프롬프트인젝션→Prompt Injection, 도구사용→Tool Use, 함수호출→Function Calling, 추론최적화→Inference Optimization, 양자화→Quantization, 캐싱→Caching, 레드팀→Red Team, 프라이버시→Privacy, 임베딩→Embedding, 벡터검색→Vector Search, 청킹→Chunking, MCP→MCP, LoRA→LoRA, RLHF→RLHF
6. Vercel 재배포
vercel deploy --prod
7. 완료 보고
수집/스크리닝/요약/번역 각 단계 결과 요약 출력.