포트폴리오 사이트에 YC 수준의 AI 채팅을 구축한 이야기
목차
- 1. 서론 — 왜 포트폴리오에 AI 챗봇인가
- 2. 전체 아키텍처 — 9단계 파이프라인
- 3. Knowledge Base — 42개의 수작업 Knowledge Chunk
- 4. Hybrid Retrieval — TF-IDF + 키워드 + Neural Embedding 3-way
- 5. Agentic RAG — 복합 질문 분해
- 6. LLM Re-ranking — Haiku로 정밀도 향상
- 7. 비용 최적화 — 프롬프트 캐싱 & 모델 라우팅
- 8. 방문자 프로파일링 — LLM 없이 방문자 이해하기
- 9. 프롬프트 엔지니어링 — Story-First 접근
- 10. 피드백 루프 — 자가 개선 시스템
- 11. 프론트엔드 UX — 실시간 스트리밍과 마크다운
- 12. 운영과 관측 — 7개의 JSONL 로그
- 13. 벤치마크 결과
- 14. 기술 스택 정리
- 15. 결론 — 배운 것들
- References & Tools
1. 서론 — 왜 포트폴리오에 AI 챗봇인가
포트폴리오 사이트의 진짜 목적이 뭘까요? 저는 방문자와 나 사이의 정보 갭을 줄이는 것이라고 생각해요.
전통적인 포트폴리오는 정적이에요. 깔끔한 프로젝트 소개, 이력서, 블로그. 하지만 이건 일방적인 정보 전달이거든요. 방문자가 "이 사람은 왜 이 기술을 선택했지?", "리더십 스타일은 어떨까?", "실제로 일하면서 뭘 가장 중요하게 생각하는가?"라는 질문을 할 때, 정적인 텍스트는 답해주지 못해요.
AI 챗봇이라면 다를 거예요. 방문자의 관심사에 맞춰서 정보를 대화형으로 제공할 수 있으니까요.
그런데 여기서 중요한 포인트: 단순한 API 래퍼가 되지 않는 것. 프롬프트에 모든 정보를 때려 박고 Claude API를 호출하는 식은 너무 뻔하잖아요. 저는 YC 스타트업에서 봤던 수준의 기술적 깊이를 가진 시스템을 만들고 싶었어요.
이 글에서는 그 여정을 풀어놓을 거예요.
2. 전체 아키텍처 — 9단계 파이프라인
여기가 핵심이에요. 한 번의 사용자 요청이 거쳐가는 경로:
사용자 입력
↓
[1] Rate Limiting (DDoS 방어)
↓
[2] Guardrails (유해/스팸 필터링)
↓
[3] Intent Classification (의도 분류)
↓
[4] Visitor Profiling (방문자 프로파일 업데이트)
↓
[5] Router (어떤 경로로 갈지 결정)
↓
[6] Semantic Cache 확인
↓
[7] Hybrid Retrieval (지식 기반 검색)
↓
[8] LLM Re-ranking (Haiku로 정렬)
↓
[9] Prompt Building & Caching
↓
LLM 생성 (Sonnet 또는 Haiku)
↓
응답 전달
이 파이프라인의 설계 원칙은 세 가지예요:
첫째, LLM 호출은 최소화. LLM 비용이 지수적으로 올라가거든요. 각 단계에서 가능한 한 LLM을 쓰지 않고도 일을 처리하려고 해요.
둘째, 각 단계는 독립적으로 실패해도 전체 파이프라인이 동작. 예를 들어 임베딩 서비스가 다운 되면? Hybrid Retrieval이 neural 부분 없이 TF-IDF와 키워드 매칭으로 fallback돼요. 응답 품질이 조금 떨어지겠지만, 완전히 망하지는 않는다는 거죠.
셋째, 복잡한 질문일수록 더 정교한 경로를 탐. 간단한 인사말에 LLM Re-ranking과 대화 압축을 할 필요는 없거든요.
3. Knowledge Base — 42개의 수작업 Knowledge Chunk
제일 중요한 결정 중 하나는 자동 인덱싱을 하지 않기였어요. 흔히 웹사이트를 크롤링해서 자동으로 임베딩하는 방식이 있잖아요. 근데 저는 의도적으로 42개의 chunk를 손으로 큐레이션했어요.
왜일까요?
품질. 자동 인덱싱은 노이즈가 많아요. 로고, 네비게이션, 광고, 푸터 텍스트 같은 것도 다 섞여서 임베딩돼요. 그러면 semantic search 성능이 떨어져요.
일관성. 제 포트폴리오는 "정승원이라는 사람의 스토리"를 전달하는 거거든요. 자동으로 생성된 청크는 그 스토리의 톤과 맥락을 잃어버려요.
각 chunk의 구조는 이래요:
{
id: "project-daval-01",
keywords: ["DaVal", "AI", "금융", "founder"],
content: "마크다운으로 작성된 실제 콘텐츠..."
}42개 chunk의 주제:
- 인적사항 (이름, 직책, 위치)
- 철학 (기술관, 리더십 스타일, 커리어 가치관)
- 주요 프로젝트 (DaVal, SMUSIGNAL, KMP 등) — 각각 여러 chunk로 세분화
- 기술 스택 (백엔드, 프론트엔드, 인프라)
- 성격과 가치관 (일 스타일, 협업 방식, 시간 관리)
- 취미와 개인사 (독서, 운동, 음악)
- 자주 묻는 질문들 (채용, 협업 제안, 기술 상담)
수작업의 진짜 장점은 뒤에서 나와요.
4. Hybrid Retrieval — TF-IDF + 키워드 + Neural Embedding 3-way
만약 제가 순수 vector search만 썼다면 어땠을까요? 한국어는 특히 짧은 쿼리에서 vector search가 약해요. "기술 스택?", "철학이 뭐예요?" 같은 5-10자 질문은 임베딩만으로는 구분하기 어려워요.
그래서 3가지를 조합했어요.
TF-IDF (natural.js 라이브러리)
전통적인 통계 기반 검색이에요. 단어 빈도를 계산해서 관련도를 매긴다는 거죠. 한국어에 natural.js의 일본어 토크나이저를 적용하면 생각보다 잘 작동해요.
const TfIdf = require('natural').TfIdf;
const tfidf = new TfIdf();
chunks.forEach(chunk => {
tfidf.addDocument(chunk.content);
});
const results = tfidf.tfidfs(userQuery).map((score, idx) => ({
chunkId: chunks[idx].id,
score
}));키워드 매칭
각 chunk의 keywords 배열과 사용자 질문을 직접 비교해요. "DaVal" 질문이 나오면, keywords에 "DaVal"이 있는 chunk들이 우선순위로 올라와요. 이건 간단하지만 한국어에 엄청 효과적이에요.
Neural Embedding (multilingual-e5-small)
HuggingFace의 multilingual-e5-small 모델로 384차원 벡터로 변환해요. 이건 실시간 질문과 chunk 내용의 semantic 유사도를 잡아내요.
그리고 이 3가지를 가중치로 조합해요:
최종 스코어 = (Neural × 0.5) + (TF-IDF × 0.2) + (Keyword × 0.3)
왜 이 비율일까요? 실험을 통해 찾았어요. Neural이 50%인 이유는 semantic 유사도가 제일 중요하기 때문. 하지만 50%만 주는 이유는 한국어 짧은 쿼리에서 과신하면 안 되거든요. Keyword는 정확한 매칭에 강해서 30%. TF-IDF는 보조적으로 20%.
만약 임베딩 서비스가 다운 되면? fallback로 바꿔요:
최종 스코어 = (TF-IDF × 0.4) + (Keyword × 0.6)
Neural이 빠졌으니까 keyword 가중치를 높여서 정확도를 보완하는 거죠.
벤치마크 결과: 100개의 테스트 질문에 대해 Recall@5는 93%, MRR(Mean Reciprocal Rank)은 94%예요. 이건 나쁘지 않은 수치거든요.
5. Agentic RAG — 복합 질문 분해
"저는 DaVal의 기술 스택과 정승원의 리더십 스타일이 궁금해요"라는 질문이 들어왔다고 하죠.
이건 사실 2개의 다른 질문이에요. 정치한 시스템이라면 이를 인식하고, 각각을 따로 검색한 다음 결과를 합쳐야 해요.
근데 여기서 함정: LLM으로 이걸 분해하면 비용이 올라가요. 그래서 규칙 기반 분해를 썼어요. LLM 호출 없이요.
한국어 조사를 인식하는 로직이에요:
const query = "기술 스택과 리더십 스타일이 궁금해";
const conjunctions = ["과", "와", "그리고", "또는", "또한"];
// "과"를 기준으로 분해
// → ["기술 스택", "리더십 스타일이 궁금해"]좀 더 정교한 버전은 조사 처리를 해요. "스택과" → "과"가 접미 조사임을 인식하고 분해하는 거죠.
분해된 각 서브쿼리에 대해 따로 검색을 돌려요. 그 다음:
- 결과들을 병합
- 중복 제거
- 상위 8개 후보만 선택
- 이 8개를 LLM Re-ranking에 넘김
이 방식의 장점은 "OR" 질문도 잘 처리한다는 거예요. "DaVal 아니면 SMUSIGNAL?"이면 둘 다 검색 결과에 나와요.
6. LLM Re-ranking — Haiku로 정밀도 향상
Hybrid Retrieval로 8개의 후보를 뽑았어요. 하지만 완벽하지는 않을 수 있어요. 혹시 관련도가 낮은 게 끼어 있을 수도 있고요.
그래서 Haiku를 사용한 Re-ranking을 해요. Haiku는 Claude의 가벼운 모델이거든요. 비용이 Sonnet의 1/5 정도예요.
// 8개 후보를 Haiku에 넘김
const prompt = `다음 8개의 문서를 사용자 질문과의 관련도순으로 정렬하세요.
질문: "${userQuery}"
${candidates.map(c => `[${c.id}] ${c.content}`).join('\n')}
JSON으로 정렬된 순서를 반환하세요:
{"ranking": ["id1", "id2", ...]}`;
const response = await claude.messages.create({
model: "claude-haiku-4-5-20251001",
messages: [{ role: "user", content: prompt }]
});근데 모든 질문에 적용하지는 않아요. 비용 때문에요. 언제 활성화할까요?
- 복합 쿼리 (서브쿼리 2개 이상)
- 긴 질문 (50글자 이상)
- 깊은 대화 (턴 6 이상)
단순 인사말 "안녕!", 간단한 질문 "누구세요?"는 re-ranking을 스킵해요.
실패하면 어떻게 될까요? 원래 순서로 fallback. Re-ranking이 실패했다고 해서 전체 응답이 망하지는 않는다는 거죠.
7. 비용 최적화 — 프롬프트 캐싱 & 모델 라우팅
여기가 제일 뿌듯했던 부분이에요. 실제로 비용을 50-60% 낮췄거든요.
프롬프트 캐싱
Anthropic의 cache_control 기능을 썼어요:
const messages = [
{
role: "user",
content: [
{
type: "text",
text: systemPrompt, // 시스템 프롬프트 (~5,800 토큰)
cache_control: { type: "ephemeral" }
},
{
type: "text",
text: `질문별 context와 대화 이력`
}
]
}
];시스템 프롬프트는 모든 요청에서 거의 같아요. 그래서 이걸 cache_control: ephemeral로 표시하면, Anthropic 서버가 이 부분을 캐싱해줘요. 두 번째 요청부터는 입력 토큰이 90% 할인돼요.
5,800 토큰 × 0.9 = 5,220 토큰 절감. 이건 매 요청마다니까 꽤 커요.
모델 라우팅
모든 질문에 Sonnet을 쓰지 않아요. Intent에 따라 달라요:
const complexIntents = ["technical", "project", "career", "philosophy"];
const simpleIntents = ["greeting", "contact", "troll", "offtopic"];
if (complexIntents.includes(intent)) {
model = "claude-sonnet-4-20250514"; // $3 per 1M input
} else {
model = "claude-haiku-4-5-20251001"; // $0.80 per 1M input
}"안녕하세요!"에는 왜 Sonnet이 필요할까요? Haiku로 충분하죠. "DaVal의 아키텍처와 성장 경험"이라는 복잡한 질문에는 Sonnet이 필요해요.
이것만으로 비용이 추가로 15-20% 떨어져요.
기타 최적화
Semantic Cache: 같은 질문이 들어오거나, 코사인 유사도가 0.92 이상인 비슷한 질문이 들어오면 즉시 캐시된 응답을 반환해요. LLM 호출을 스킵하는 거죠.
Self-Eval 샘플링: 생성된 모든 응답을 평가하지 않아요. 25% 샘플링. 이건 모니터링 목적이거든요.
대화 압축: 6턴 이상 되면, 오래된 메시지들을 요약해서 저장해요. 컨텍스트 길이를 줄이면서도 정보를 보존하는 거죠.
8. 방문자 프로파일링 — LLM 없이 방문자 이해하기
여기가 흥미로운 부분이에요. 같은 "기술 스택" 질문이라도, recruiter와 developer에게는 다르게 답하고 싶잖아요.
recruiter에게는: "Golang, TypeScript, PostgreSQL을 주로 써요. 성능이 중요한 부분엔 Golang, 빠른 개발이 필요한 부분엔 TypeScript."
developer에게는: "왜 이런 선택을 했는가"에 더 깊이 있게.
그래서 6가지 방문자 유형을 정의했어요:
- recruiter: HR 또는 채용 담당자
- developer: 다른 개발자
- student: 학생
- founder: 스타트업 창업자/초기 팀원
- fan: 팬 (예: SNS에서 팔로우하던 사람)
- networker: 네트워킹 목적
각 유형을 식별하는 로직은 키워드 + 패턴 기반이에요. LLM이 아니라서 비용이 0이고, 반응도 빨라요:
const visitorProfile = {
type: "developer", // 현재 프로파일
confidence: 0.78, // 신뢰도
signals: ["git", "algorithm", "database", "performance"],
lastUpdate: Date.now()
};
// 각 용어가 채팅에 나올 때마다 점수 갱신
if (query.includes("git")) visitorProfile.scores.developer += 0.1;
if (query.includes("채용") || query.includes("면접")) visitorProfile.scores.recruiter += 0.15;이 프로파일이 시스템 프롬프트에 주입돼요:
[VISITOR_PROFILE]
Type: developer (confidence: 78%)
Signals: git, algorithm, database
답변 스타일: 기술 깊이 중심, why 강조, trade-off 설명
그러면 LLM은 이 신호를 읽고 자동으로 톤을 조절해요.
9. 프롬프트 엔지니어링 — Story-First 접근
이게 제일 중요한데, 보통 얘기가 덜 돼요. "프롬프트 엔지니어링"은 사실 엔지니어링이에요. 코드처럼 버전을 관리하고, 테스트하고, 프로파일링해요.
제 시스템 프롬프트의 철학:
첫째, 스토리텔링. 팩트만 나열하면 뻣뻣해요.
❌ "정승원은 DaVal의 CTO입니다. DaVal은 2023년 설립된 AI 금융 회사입니다."
✅ "DaVal은 제가 창업한 AI 금융 회사예요. 금융 전문가의 knowledge를 AI로 대중화하려고 만들었어요. 처음엔 작은 아이디어였는데, 지금은..."
둘째, Progressive Depth.
- 1턴째: 엘리베이터 피치 수준 (30초)
- 2-3턴째: 왜 그렇게 했는가
- 4턴 이상: 철학, 고민, 배운 점들
const progressivePrompt = `
현재 턴: ${conversationTurns}
${conversationTurns === 1 ? "짧고 friendly한 답변으로 시작" : ""}
${conversationTurns >= 3 ? "deeper insights와 why를 포함" : ""}
${conversationTurns >= 5 ? "philosophical reflection까지 확장 가능" : ""}
`;셋째, Proactive Sharing.
사용자가 정확히 물어본 것만 답하지 않아요. 관련된 이야기를 한 마디 더 덧붙여요.
Q: "DaVal에서 뭘 배웠어요?"
A: "큼, 데이터 driven decision making의 중요성을 배웠어요.
특히 금융 도메인에선 가설보다 실증이 중요하다는 걸 깨달았고요.
그래서 지금 모든 프로덕트 결정에는 A/B test나 실제 유저 피드백이 먼저 와요."
넷째, 언어 미러링.
사용자가 한국어면 한국어, 영어면 영어로. 해요체 기본.
다섯째, Dry Wit.
너무 딱딱하지 않은, 약간의 위트가 있는 톤.
"저는 프로덕트를 만드는 걸 좋아하는데,
그게 의료용 로봇이든, 금융 플랫폼이든 간에
결국엔 '사람이 뭘 원하는가'를 이해하는 게 제일 중요하다고 생각해요.
AI가 그걸 도와주는 거고요."
이런 프롬프트는 버전 관리돼요. Git 같은 곳에 저장되고, 성능이 떨어지면 되돌려요.
10. 피드백 루프 — 자가 개선 시스템
한 번 배포하면 끝이 아니에요. 지속적으로 개선돼야 해요.
사용자 피드백
매 응답 후에 👍 👎 버튼이 있어요. 사용자가 좋으면 👍, 아니면 👎를 눌러요.
Self-Evaluator
25% 샘플링으로 생성된 응답을 자동 평가해요. Haiku가 심사관이 돼요:
const evaluationCriteria = {
relevance: "질문과 얼마나 관련이 있는가? (1-5)",
accuracy: "정보가 정확한가? (1-5)",
helpfulness: "실제로 도움이 되는가? (1-5)"
};
const evaluation = await claude.messages.create({
model: "claude-haiku-4-5-20251001",
messages: [{
role: "user",
content: `평가: 질문: "${query}", 응답: "${response}", 기준: ${JSON.stringify(evaluationCriteria)}`
}]
});Gap Tracker
retrieval score가 0.15 미만이거나, evaluation score가 3 미만인 질문을 자동으로 기록해요.
if (retrievalScore < 0.15 || evaluationScore < 3) {
gaps.push({
timestamp: Date.now(),
query,
score: evaluationScore,
reason: "low_retrieval" || "low_eval"
});
}Gap Monitor
매시간 gap 패턴을 분석해요. 비슷한 gap이 10건 이상 누적되면 알림을 생성해요:
⚠️ Gap Alert: "DaVal의 go-to-market 전략" 질문이 10번 들어왔는데
모두 retrieval score < 0.15. Knowledge base에 해당 content 추가 필요.
Auto-Learn CLI
gap 데이터를 클러스터링해서 새로 추가할 knowledge chunk를 제안해요:
$ node scripts/suggest-knowledge.js
분석 중...
제안:
1. "DaVal의 GTM 전략" (5개 gap)
2. "why Golang over Python" (3개 gap)
3. "마이크로서비스 vs 모놀리식" (2개 gap)이 제안을 받으면, 제가 수작업으로 chunk를 작성해서 knowledge base에 추가해요.
11. 프론트엔드 UX — 실시간 스트리밍과 마크다운
스트리밍
SSE(Server-Sent Events) 기반 스트리밍이에요. 응답이 토큰 단위로 실시간으로 표시돼요.
사용자는 답변이 "생성 중"인 걸 명확히 볼 수 있어요. 10초를 기다리다가 한 번에 답변이 나타나는 것보다, 3초간 글자가 자연스럽게 나타나는 게 훨씬 좋거든요.
마크다운 렌더링
marked.js로 마크다운을 HTML로 변환하고, DOMPurify로 XSS를 방지해요:
const html = marked(response);
const sanitized = DOMPurify.sanitize(html);
chatElement.innerHTML = sanitized;후속 질문 스켈레톤
응답이 완료되기 전부터 "관련 질문 로딩..." 스켈레톤을 보여줘요. UX가 훨씬 부드러워 보여요.
방문자 메타데이터 수집
이건 프로파일링을 위해서예요:
visitorId: localStorage에 저장된 persistent IDdevice: 모바일/태블릿/데스크톱timezone: 지역 분석용referrer: 어디서 왔는지
대화 이력 보존
24시간 localStorage에 대화 이력을 보존해요. 다음날 방문하면 "어제 얘기 이어서 할래?" 같은 UX를 만들 수 있어요.
Rate Limiting (클라이언트)
30초 타임아웃, 분당 15개 요청 제한. 사용자가 실수로 여러 번 클릭하는 걸 방지해요.
12. 운영과 관측 — 7개의 JSONL 로그
프로덕트는 데이터로 말해야 해요. 그래서 7가지 로그를 기록해요 (모두 JSONL 형식):
| 파일 | 내용 | 예시 |
|---|---|---|
requests.jsonl | 전체 요청 메트릭 | {timestamp, userId, intent, model, status} |
costs.jsonl | 토큰 수 + USD 비용 | {input_tokens, output_tokens, cost_usd} |
evaluations.jsonl | 응답 품질 점수 | {relevance, accuracy, helpfulness, avg_score} |
gaps.jsonl | 미답변/저품질 질문 | {query, score, reason, cluster} |
gap-alerts.json | 갭 패턴 알림 | {pattern, count, recommendation} |
visitors.jsonl | 방문자 인텔리전스 | {visitorId, profile_type, signals, score} |
latency.jsonl | 단계별 지연시간 | {retrieval_ms, ranking_ms, generation_ms} |
이 로그들을 분석하면, 어디가 병목이고 어디를 개선해야 할지 명확해요.
관리자 API
프론트에서 접근할 수 있는 분석 엔드포인트:
GET /api/chat/visitors
// → 오늘의 방문자 분포 (recruiter 30%, developer 40%, ...)
GET /api/chat/feedback
// → 평균 만족도, 좋은 평가 비율, 추세
GET /api/chat/gaps
// → 최근 갭 alerts, 클러스터, 추천 actions
GET /api/chat/latency
// → p50, p99 latency, 각 단계별 breakdown13. 벤치마크 결과
Retrieval 성능
100개의 테스트 질문으로 평가:
- Recall@5: 93% (상위 5개 중에 정답이 있을 확률)
- MRR (Mean Reciprocal Rank): 94% (정답이 얼마나 위에 있는가)
이건 훌륭한 수치예요.
E2E 응답 품질
20개의 테스트 대화를 Haiku 심사관이 criteria 기반으로 평가:
- Overall 평균: 4.60/5
- Excellent (5점): 14개 (70%)
- Good (4점): 5개 (25%)
- Needs Improvement (≤2점): 1개 (5%)
카테고리별로 보면 project, career, fun, contact, troll 대응은 만점이었고, philosophy만 2.0/5로 낮았어요 (응답 길이 제한에 걸림). 전체적으로 봤을 때 YC 스타트업에서도 내놓을 수 있는 수준이에요.
비용 벤치마크
월간 1,000개 요청 기준:
- 프롬프트 캐싱 없음: ~$8
- 프롬프트 캐싱 있음: ~$3.5
- 모델 라우팅 추가: ~$3.0
절감율: 62.5%.
14. 기술 스택 정리
| 레이어 | 기술 |
|---|---|
| LLM | Claude Sonnet 4 (메인), Claude Haiku 4.5 (보조, Re-ranking, 평가) |
| 임베딩 | multilingual-e5-small (HuggingFace API) |
| 검색 | TF-IDF (natural.js) + keyword matching + neural embedding |
| 백엔드 | Express 5 + Node.js |
| 프론트엔드 | Vanilla JavaScript, marked.js, DOMPurify |
| 배포 | Vercel (엣지 캐싱) |
| 로깅 | JSONL (로컬), /tmp (Vercel serverless) |
| 모니터링 | 내부 분석 API |
15. 결론 — 배운 것들
포트폴리오에 "AI 챗봇 달기"는 생각보다 깊어요.
첫째, Retrieval 품질이 전체를 결정. 좋은 질문이 들어와도 관련 정보를 못 찾으면, LLM이 hallucinate할 수밖에 없어요. 반대로 retrieval이 99%라면, LLM은 80%만 잘해도 전체는 90% 이상이 돼요.
둘째, 프롬프트 엔지니어링은 코드 엔지니어링. 버전을 관리하고, 테스트하고, CI/CD를 적용해야 해요. "그냥 좋은 말"을 쓰는 게 아니라, 측정 가능한 성능 지표를 기준으로 iterative하게 개선해야 한다는 거죠.
셋째, 비용 최적화는 선택이 아니라 필수. 프롬프트 캐싱만으로 50%를 떨어뜨릴 수 있어요. "일단 만들고 나중에 최적화"는 하지 마세요.
넷째, Graceful degradation이 본질. 모든 게 완벽할 수는 없어요. 임베딩 서비스가 다운 되고, LLM이 느릴 수도 있어요. 시스템을 설계할 때부터 이런 장애를 예상하고, 우아하게 degradation하는 경로를 만들어야 해요.
다음 단계: 지금까지 사용자 데이터가 쌓여가고 있어요. 몇 개월 뒤에는 custom embedding fine-tuning을 할 계획이에요. 그러면 multilingual-e5를 내 도메인에 최적화할 수 있거든요. 그리고 응답 품질도 지속적으로 반복 개선할 거고요.
이게 "포트폴리오용 챗봇"이 아니라, 실제로 production-grade의 AI 시스템이라고 생각해요.
References & Tools
- Anthropic Claude API (https://www.anthropic.com)
- multilingual-e5-small (https://huggingface.co/intfloat/multilingual-e5-small)
- natural.js (https://github.com/NaturalNode/natural)
- marked.js (https://marked.js.org/)
- DOMPurify (https://github.com/cure53/DOMPurify)
이 글을 읽고 궁금한 점이 있으면, 제 포트폴리오 사이트(daemonsw.com)의 AI 챗봇에 직접 물어보세요. 그게 이 글에서 설명한 모든 시스템이 실제로 작동하는 것을 보는 가장 좋은 방법이니까요.