2026๋…„ 2์›” 23์ผ
Architecture

๐Ÿง  OpenPersona v2.0 โ€” RAG ํŒŒ์ดํ”„๋ผ์ธ๊ณผ AI ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๊ตฌ์ถ•๊ธฐ

OpenPersona v2.0์—์„œ Vectra ๊ธฐ๋ฐ˜ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰(๋ฒกํ„ฐ + BM25), RRF ๋ณ‘ํ•ฉ, LLM ๋ฆฌ๋žญํ‚น์œผ๋กœ ๊ตฌ์„ฑ๋œ RAG ํŒŒ์ดํ”„๋ผ์ธ๊ณผ Intent ๋ถ„๋ฅ˜ โ†’ Model ์„ ํƒ โ†’ Tool Call Loop๊นŒ์ง€ AI Agent Orchestrator๋ฅผ ์‹ค์ œ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์ƒ์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

AI Agent
RAG
Orchestration
Electron
TypeScript
Gemini
Vector Search
BM25
Reranking
LLM
๐Ÿง  OpenPersona v2.0 โ€” RAG ํŒŒ์ดํ”„๋ผ์ธ๊ณผ AI ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๊ตฌ์ถ•๊ธฐ

๐Ÿ“… ๊ธ€ ๊ฐœ์š”

์ด์ „ ๊ธ€์—์„œ OpenPersona v1์˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์†Œ๊ฐœํ•˜๋ฉฐ, ๋งˆ์ง€๋ง‰์— ์ด๋Ÿฐ ์ฒญ์‚ฌ์ง„์„ ๊ทธ๋ ธ์Šต๋‹ˆ๋‹ค.

"v2์—์„œ๋Š” AI Agent Orchestrator๋ฅผ ์ค‘์‹ฌ์œผ๋กœ RAG์™€ MCP๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ ํ›จ์”ฌ ๊ฐ•๋ ฅํ•œ ์—์ด์ „ํŠธ๋กœ ์ง„ํ™”ํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค."

๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ ํ•ด๋ƒˆ์Šต๋‹ˆ๋‹ค. +3,917์ค„์˜ ์ฝ”๋“œ, 35๊ฐœ ํŒŒ์ผ ๋ณ€๊ฒฝ โ€” ๋‹จ์ˆœ ์ฑ„ํŒ…๋ด‡์ด์—ˆ๋˜ v1์ด ์ง€์‹์„ ๊ฒ€์ƒ‰ํ•˜๊ณ , ์˜๋„๋ฅผ ํŒŒ์•…ํ•˜๊ณ , ๋„๊ตฌ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์ง„์งœ AI ์—์ด์ „ํŠธ๋กœ ํƒˆ๋ฐ”๊ฟˆํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ๊ทธ ์—ฌ์ •์„ ํ•จ๊ป˜ ๋”ฐ๋ผ๊ฐ€ ๋ด…๋‹ˆ๋‹ค. ํŠนํžˆ "RAG๊ฐ€ ๋ญ”๋ฐ?", "์™œ ๊ฒ€์ƒ‰์„ ๋‘ ๊ฐ€์ง€๋กœ ํ•˜๋Š” ๊ฑฐ์•ผ?", "๋ฆฌ๋žญํ‚น์ด ๋ญ์•ผ?" ๊ฐ™์€ ์งˆ๋ฌธ์— ์ฝ”๋“œ์™€ ๋น„์œ ๋ฅผ ์„ž์–ด ๋‹ตํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ์ด ๊ธ€์—์„œ ๋‹ค๋ฃฐ ๋‚ด์šฉ

  • RAG(Retrieval-Augmented Generation) ํŒŒ์ดํ”„๋ผ์ธ ์„ค๊ณ„์™€ ๊ตฌํ˜„
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰: ๋ฒกํ„ฐ ์‹œ๋งจํ‹ฑ + BM25 ํ‚ค์›Œ๋“œ
  • RRF(Reciprocal Rank Fusion)๋กœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ณ‘ํ•ฉ
  • Gemini Flash ๊ธฐ๋ฐ˜ ๊ฒฝ๋Ÿ‰ LLM ๋ฆฌ๋žญํ‚น
  • Structure-Aware ์ฒญํ‚น๊ณผ ํ† ํฐ ์ตœ์ ํ™” ์ „๋žต
  • AI Agent Orchestrator: Intent ๋ถ„๋ฅ˜ โ†’ Model ์„ ํƒ โ†’ Tool Call Loop

๐Ÿ—๏ธ v2.0 ์ „์ฒด ์•„ํ‚คํ…์ฒ˜

๋จผ์ € ํฐ ๊ทธ๋ฆผ๋ถ€ํ„ฐ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. v1์—์„œ๋Š” ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๊ฐ€ LLM์— ๋ฐ”๋กœ ์ „๋‹ฌ๋˜์—ˆ์ง€๋งŒ, v2์—์„œ๋Š” Orchestrator๊ฐ€ ์ค‘์•™ ํ—ˆ๋ธŒ๊ฐ€ ๋˜์–ด ๋ชจ๋“  ์š”์ฒญ์„ ์กฐ์œจํ•ฉ๋‹ˆ๋‹ค.

OpenPersona v2.0 ์•„ํ‚คํ…์ฒ˜ - RAG Pipeline๊ณผ AI Agent Orchestrator

v1๊ณผ ๋น„๊ตํ•˜๋ฉด, ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๊ฐ€ LLM์— ๋„๋‹ฌํ•˜๊ธฐ ์ „์— 4๊ฐœ์˜ ์ƒˆ๋กœ์šด ๋ ˆ์ด์–ด๋ฅผ ๊ฑฐ์นฉ๋‹ˆ๋‹ค:

๊ตฌ์„ฑ ์š”์†Œ ์—ญํ• 
Intent Classifier "์ด ์งˆ๋ฌธ์ด ๋ฒˆ์—ญ์ธ์ง€, ์ฝ”๋“œ ๋ฆฌ๋ทฐ์ธ์ง€, ํŒŒ์ผ ์ž‘์—…์ธ์ง€" ์˜๋„๋ฅผ ํŒŒ์•…
RAG Engine ์บ๋ฆญํ„ฐ๋ณ„ ์ „๋ฌธ ์ง€์‹์—์„œ ๊ด€๋ จ ์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ํ•ด LLM์— ์ฃผ์ž…
Model Selector ์˜๋„์— ๋งž๋Š” ์ตœ์  LLM ๋ชจ๋ธ์„ ๋™์ ์œผ๋กœ ์„ ํƒ
Tool Registry ํŒŒ์ผ์‹œ์Šคํ…œ ์กฐ์ž‘, ์—‘์…€ ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋“ฑ 11์ข…์˜ ๋„๊ตฌ๋ฅผ ๊ด€๋ฆฌ

์ด ๊ตฌ์กฐ ๋•๋ถ„์— Felix(์—ฌ์šฐ)์—๊ฒŒ ์ฝ”๋“œ ์งˆ๋ฌธ์„ ํ•˜๋ฉด React/TypeScript ์ง€์‹ ๊ธฐ๋ฐ˜์œผ๋กœ, Done(๋ผ์ง€)์—๊ฒŒ ์—‘์…€ ์งˆ๋ฌธ์„ ํ•˜๋ฉด ์—‘์…€ ํ•จ์ˆ˜/ํ”ผ๋ฒ— ์ง€์‹ ๊ธฐ๋ฐ˜์œผ๋กœ, Bomi(ํ† ๋ผ)์—๊ฒŒ ๋ฒˆ์—ญ์„ ๋ถ€ํƒํ•˜๋ฉด ๋ฒˆ์—ญ ํŒจํ„ด/ํ†ค ๊ฐ€์ด๋“œ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€์ด ๋‚˜์˜ต๋‹ˆ๋‹ค. ๊ฐ™์€ ํŒŒ์ดํ”„๋ผ์ธ์ธ๋ฐ, ์บ๋ฆญํ„ฐ๋งˆ๋‹ค ์ „ํ˜€ ๋‹ค๋ฅธ ์ „๋ฌธ ๋‹ต๋ณ€์ด ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฑฐ์ฃ .


๐Ÿ” ์‹ค์ œ ์งˆ์˜๋กœ ๋”ฐ๋ผ๊ฐ€๋ณด๋Š” RAG ํŒŒ์ดํ”„๋ผ์ธ

์„ค๋ช…๋ณด๋‹ค๋Š” ์ง์ ‘ ๋ณด๋Š” ๊ฒŒ ๋น ๋ฆ…๋‹ˆ๋‹ค. ๋‘ ๊ฐ€์ง€ ์‹ค์ œ ์งˆ์˜๊ฐ€ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€ ๋‹จ๊ณ„๋ณ„๋กœ ๋”ฐ๋ผ๊ฐ€ ๋ด…์‹œ๋‹ค.

์˜ˆ์‹œ 1: "๐Ÿท Done, ์—‘์…€์—์„œ VLOOKUP ์‚ฌ์šฉ๋ฒ• ์•Œ๋ ค์ค˜"

Step 1 โ€” Intent ๋ถ„๋ฅ˜

Orchestrator๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์œผ๋ฉด, ๊ฐ€์žฅ ๋จผ์ € Intent Classifier๊ฐ€ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค:

// intent-classifier.ts โ€” ํ‚ค์›Œ๋“œ ํŒจํ„ด ๋งค์นญ
const KEYWORD_RULES = [
  { pattern: /์–ด๋–ป๊ฒŒ|๋ฐฉ๋ฒ•|์‚ฌ์šฉ๋ฒ•|๋ฌธ๋ฒ•|ํ•จ์ˆ˜|API/i, 
    type: 'knowledge_query', needsKnowledge: true, needsTool: false },
  // ...
];

// "์‚ฌ์šฉ๋ฒ•"์ด ๋งค์นญ โ†’ knowledge_query ์˜๋„๋กœ ๋ถ„๋ฅ˜
// ์บ๋ฆญํ„ฐ๊ฐ€ pig(Done)์ด๋ฏ€๋กœ category: 'excel' ์ถ”๋ก 

๊ฒฐ๊ณผ: { type: 'knowledge_query', category: 'excel', needsKnowledge: true, confidence: 0.9 }

Step 2 โ€” RAG ๊ฒ€์ƒ‰ (ํ•ต์‹ฌ!)

needsKnowledge: true์ด๋ฏ€๋กœ RAG Engine์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. "VLOOKUP ์‚ฌ์šฉ๋ฒ•"์ด๋ผ๋Š” ์ฟผ๋ฆฌ๋กœ pig ์บ๋ฆญํ„ฐ์˜ ์ง€์‹ ์ธ๋ฑ์Šค๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

[๊ฒ€์ƒ‰ ๋Œ€์ƒ] pig/excel/functions.md, pig/excel/advanced-formulas.md ...
[์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰] "VLOOKUP ์‚ฌ์šฉ๋ฒ•"์˜ ์˜๋ฏธ์™€ ๊ฐ€์žฅ ์œ ์‚ฌํ•œ ์ฒญํฌ top-10
[ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰] "VLOOKUP"์ด๋ผ๋Š” ์ •ํ™•ํ•œ ๋‹จ์–ด๊ฐ€ ๋“ค์–ด๊ฐ„ ์ฒญํฌ top-10
[RRF ๋ณ‘ํ•ฉ] ๋‘ ๊ฒฐ๊ณผ๋ฅผ ํ•ฉ์ณ์„œ ์ตœ์ข… top-10
[LLM ๋ฆฌ๋žญํ‚น] Gemini Flash๊ฐ€ top-10์—์„œ ๊ฐ€์žฅ ๊ด€๋ จ ๋†’์€ top-5 ์„ ๋ณ„

์ตœ์ข…์œผ๋กœ ์ด๋Ÿฐ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ถ”์ถœ๋ฉ๋‹ˆ๋‹ค:

[1] ## VLOOKUP ํ•จ์ˆ˜
    ๊ธฐ๋ณธ ๋ฌธ๋ฒ•: =VLOOKUP(์ฐพ๋Š”๊ฐ’, ๋ฒ”์œ„, ์—ด๋ฒˆํ˜ธ, [์ผ์น˜์œ ํ˜•])
    ๋งค๊ฐœ๋ณ€์ˆ˜ ์„ค๋ช…: ...
[2] ## VLOOKUP vs INDEX/MATCH
    VLOOKUP์€ ์™ผ์ชฝโ†’์˜ค๋ฅธ์ชฝ๋งŒ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ, INDEX/MATCH๋Š” ์–‘๋ฐฉํ–ฅ...
[3] ## ๊ณ ๊ธ‰ ํ™œ์šฉ โ€” ๊ทผ์‚ฌ ์ผ์น˜ / ์™€์ผ๋“œ์นด๋“œ
    ...

Step 3 โ€” Context ์ฃผ์ž…

Context Builder๊ฐ€ ์ด RAG ๊ฒฐ๊ณผ๋ฅผ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค:

[System Prompt]
๋„ˆ๋Š” "Done"์ด๋ผ๋Š” ์ด๋ฆ„์˜ ๋ผ์ง€ ์บ๋ฆญํ„ฐ AI ๋ฌธ์„œ ์ „๋ฌธ๊ฐ€์•ผ...

--- ์ฐธ๊ณ  ์ง€์‹ (RAG) ---
์•„๋ž˜๋Š” ๊ด€๋ จ ์ „๋ฌธ ์ง€์‹์ž…๋‹ˆ๋‹ค. ๋‹ต๋ณ€ ์‹œ ์ ๊ทน์ ์œผ๋กœ ์ฐธ๊ณ ํ•˜๋˜, 
์ง€์‹์— ์—†๋Š” ๋‚ด์šฉ์€ ์†”์งํžˆ ๋ชจ๋ฅธ๋‹ค๊ณ  ํ•˜์„ธ์š”.

[1] ## VLOOKUP ํ•จ์ˆ˜
    ๊ธฐ๋ณธ ๋ฌธ๋ฒ•: =VLOOKUP(์ฐพ๋Š”๊ฐ’, ๋ฒ”์œ„, ์—ด๋ฒˆํ˜ธ, [์ผ์น˜์œ ํ˜•])...
[2] ## VLOOKUP vs INDEX/MATCH...
--- ์ฐธ๊ณ  ์ง€์‹ ๋ ---

Step 4 โ€” LLM ์‘๋‹ต

LLM์ด Done์˜ ์„ฑ๊ฒฉ + VLOOKUP ์ „๋ฌธ ์ง€์‹์„ ํ•ฉ์ณ์„œ ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค:

"์•ˆ๋…•ํ•˜์„ธ์šฉ~ VLOOKUP ์‚ฌ์šฉ๋ฒ•์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”! ใ…Žใ…Ž ๐Ÿท

VLOOKUP์€ ํ‘œ์—์„œ ์›ํ•˜๋Š” ๊ฐ’์„ ์ฐพ์•„์˜ค๋Š” ํ•จ์ˆ˜์˜ˆ์š”!
๊ธฐ๋ณธ ๋ฌธ๋ฒ•์€ =VLOOKUP(์ฐพ๋Š”๊ฐ’, ๋ฒ”์œ„, ์—ด๋ฒˆํ˜ธ, ์ผ์น˜์œ ํ˜•) ์ด๋ ‡๊ฒŒ ์“ฐ๋ฉด ๋ผ์š”..."

RAG๊ฐ€ ์—†์—ˆ๋‹ค๋ฉด? LLM์ด ์ผ๋ฐ˜์ ์ธ VLOOKUP ์„ค๋ช…์„ ํ–ˆ๊ฒ ์ง€๋งŒ, RAG ๋•๋ถ„์— Done์ด ํ•™์Šตํ•œ ์—‘์…€ ์ง€์‹ ๋ฌธ์„œ์˜ ์ •ํ™•ํ•œ ๋ฌธ๋ฒ•๊ณผ ํŒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€ํ•ฉ๋‹ˆ๋‹ค.


์˜ˆ์‹œ 2: "๐Ÿฐ Bomi, ์ •ํƒœ์ธ ์•ˆ๋…•ํ•˜์„ธ์š”๋ฅผ ์ผ๋ณธ์–ด๋กœ ์•Œ๋ ค์ค˜"

๊ฐ™์€ ํŒŒ์ดํ”„๋ผ์ธ์ธ๋ฐ ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค.

Step 1 โ€” Intent ๋ถ„๋ฅ˜

// "์ผ๋ณธ์–ด๋กœ"๊ฐ€ ๋งค์นญ โ†’ translation ์˜๋„
{ pattern: /ํ•œ๊ตญ์–ด๋กœ|์˜์–ด๋กœ|์ผ๋ณธ์–ด๋กœ|์ค‘๊ตญ์–ด๋กœ/i, 
  type: 'translation', needsKnowledge: true, needsTool: false }

// ์บ๋ฆญํ„ฐ๊ฐ€ rabbit(Bomi) + "์ผ๋ณธ์–ด" โ†’ category: 'ja-ko'

๊ฒฐ๊ณผ: { type: 'translation', category: 'ja-ko', needsKnowledge: true, confidence: 0.9 }

Step 2 โ€” RAG ๊ฒ€์ƒ‰

์ด๋ฒˆ์—๋Š” rabbit์˜ ja-ko ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค:

[๊ฒ€์ƒ‰ ๋Œ€์ƒ] rabbit/ja-ko/translation-patterns.md, rabbit/style-guides/tone-guide.md
[ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ โ†’ RRF โ†’ ๋ฆฌ๋žญํ‚น]

์ถ”์ถœ๋œ ์ปจํ…์ŠคํŠธ:

[1] ## ์–ด์ˆœ ์œ ์‚ฌ์„ฑ ํ™œ์šฉ
    ์ผ๋ณธ์–ด์™€ ํ•œ๊ตญ์–ด๋Š” ์–ด์ˆœ(SOV)์ด ๋™์ผ
    ์กฐ์‚ฌ ๋Œ€์‘: ใฏโ†’์€/๋Š”, ใŒโ†’์ด/๊ฐ€, ใ‚’โ†’์„/๋ฅผ...
[2] ## ๊ฒฝ์–ด ๋ณ€ํ™˜
    ~ใงใ™ โ†’ ~์ž…๋‹ˆ๋‹ค/~์ด์—์š”
    ~ใพใ™ โ†’ ~ํ•ฉ๋‹ˆ๋‹ค/~ํ•ด์š”...
[3] ## ๋ฌธํ™”์  ๋‰˜์•™์Šค
    ใ‚ˆใ‚ใ—ใใŠ้ก˜ใ„ใ—ใพใ™ โ†’ "์ž˜ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค"...

Step 3 โ€” Context ์ฃผ์ž… โ†’ LLM ์‘๋‹ต

Bomi์˜ ํ™œ๋ฐœํ•œ ์„ฑ๊ฒฉ + ์ผ๋ณธ์–ด ๋ฒˆ์—ญ ์ „๋ฌธ ์ง€์‹์ด ํ•ฉ์ณ์ ธ์„œ:

"๊นก์ด! ์ผ๋ณธ์–ด ๋ฒˆ์—ญ์ด๋ผ๋ฉด ๋‹น๊ทผ ๋‚ด๊ฐ€ ๋„์™€์ค„๊ฒŒ~! ๐Ÿฅ•โœจ

'์ •ํƒœ์ธ ์•ˆ๋…•ํ•˜์„ธ์š”'๋ฅผ ์ผ๋ณธ์–ด๋กœ ํ•˜๋ฉด:
์ •ํƒœ์ธ(ใƒใƒงใƒณใƒปใƒ†ใ‚คใƒณ)ใ•ใ‚“ใ€ใ“ใ‚“ใซใกใฏ

์ฐธ๊ณ ๋กœ ์ผ๋ณธ์–ด์—์„œ ์ธ์‚ฌํ•  ๋•Œ๋Š” ์ƒ๋Œ€๋ฐฉ ์ด๋ฆ„ ๋’ค์— 'ใ•ใ‚“'์„ 
๋ถ™์ด๋Š” ๊ฒŒ ๊ธฐ๋ณธ ์˜ˆ์˜์•ผ! ๊ฒฉ์‹์„ ์ฐจ๋ฆฌ๋ ค๋ฉด 'ๆง˜(ใ•ใพ)'๋ฅผ ์“ฐ๊ธฐ๋„ ํ•ด~"

ํ•ต์‹ฌ ํฌ์ธํŠธ: ๊ฐ™์€ RAG ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ฑฐ์ณค๋Š”๋ฐ, ๊ฒ€์ƒ‰๋˜๋Š” ์ง€์‹ ์ €์žฅ์†Œ๊ฐ€ ์™„์ „ํžˆ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. Done์€ ์—‘์…€ ํ•จ์ˆ˜ ๊ฐ€์ด๋“œ๋ฅผ, Bomi๋Š” ์ผ๋ณธ์–ด ๋ฒˆ์—ญ ํŒจํ„ด์„ ์ฐธ์กฐํ–ˆ์ฃ . ์ด๊ฒƒ์ด "์บ๋ฆญํ„ฐ๋ณ„ ์ „๋ฌธ๊ฐ€ RAG"์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.


๐Ÿ“š RAG Pipeline ๊นŠ์ด ํŒŒ๊ธฐ

์ด์ œ ๊ฐ ๋‹จ๊ณ„๋ฅผ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ์‚ดํŽด๋ด…์‹œ๋‹ค.

1. Document Ingestion โ€” ๋ฌธ์„œ ๋กœ๋”

RAG์˜ ์‹œ์ž‘์€ ๋ฌธ์„œ๋ฅผ ์ฝ์–ด๋“ค์ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. OpenPersona๋Š” ๋‹ค์–‘ํ•œ ํ˜•์‹์˜ ๋ฌธ์„œ๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ๋Š” ํ†ตํ•ฉ ๋กœ๋”๋ฅผ ๊ฐ–์ถ”๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

// document-loader/index.ts
export async function loadDocument(filePath: string): Promise<ParsedDocument> {
  const ext = path.extname(filePath).toLowerCase();

  switch (ext) {
    case '.xlsx': case '.xls': return loadExcel(filePath);
    case '.docx':               return loadDocx(filePath);
    case '.md': case '.txt':    return loadTextFile(filePath, ext);
    case '.json':               return loadJsonFile(filePath);
    default: throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: ${ext}`);
  }
}

Excel์€ ์‹œํŠธ๋ณ„๋กœ ์„น์…˜์„ ๋‚˜๋ˆ„๊ณ  Markdown ํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ํ•˜๋ฉฐ, Word๋Š” Mammoth๋กœ HTML ๋ณ€ํ™˜ ํ›„ Markdown์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ฌธ์„œ๊ฐ€ ๋™์ผํ•œ ParsedDocument ๊ตฌ์กฐ๋กœ ํ†ต์ผ๋˜์–ด ์ดํ›„ ์ฒญํ‚น ํŒŒ์ดํ”„๋ผ์ธ์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.

2. Structure-Aware Chunker โ€” ๋˜‘๋˜‘ํ•œ ๋ฌธ์„œ ๋ถ„ํ• 

Structure-Aware Chunking ํ”„๋กœ์„ธ์Šค โ€” ๋ฌธ์„œ ๊ตฌ์กฐ ๋ถ„ํ•  โ†’ 500ํ† ํฐ ๋ถ„ํ•  โ†’ 50ํ† ํฐ ์˜ค๋ฒ„๋žฉ โ†’ Vectra ์ €์žฅ

๋ฌธ์„œ๋ฅผ ํ†ต์งธ๋กœ ์ž„๋ฒ ๋”ฉํ•  ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค. LLM์˜ ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ์—๋„ ํ•œ๊ณ„๊ฐ€ ์žˆ๊ณ , ๊ฒ€์ƒ‰ ์ •ํ™•๋„๋ฅผ ์œ„ํ•ด์„œ๋„ ์ ์ ˆํ•œ ํฌ๊ธฐ๋กœ ๋‚˜๋ˆ ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

OpenPersona์˜ ์ฒญ์ปค๋Š” ๋ฌธ์„œ ๊ตฌ์กฐ๋ฅผ ์กด์ค‘ํ•˜๋ฉด์„œ ๋ถ„ํ• ํ•ฉ๋‹ˆ๋‹ค:

// chunker.ts
const DEFAULT_CONFIG: ChunkerConfig = {
  maxTokens: 500,     // ์ฒญํฌ๋‹น ์ตœ๋Œ€ 500 ํ† ํฐ
  overlapTokens: 50,  // ์—ฐ์† ์ฒญํฌ ๊ฐ„ 50 ํ† ํฐ ๊ฒน์นจ
};

์™œ 500ํ† ํฐ์ธ๊ฐ€?

  • ๋„ˆ๋ฌด ์ž‘์œผ๋ฉด (100ํ† ํฐ): ๋ฌธ๋งฅ์ด ๋Š๊ฒจ์„œ ๊ฒ€์ƒ‰ํ•ด๋„ ์˜๋ฏธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค
  • ๋„ˆ๋ฌด ํฌ๋ฉด (2000ํ† ํฐ): ๊ฒ€์ƒ‰ ์ •๋ฐ€๋„๊ฐ€ ๋–จ์–ด์ง€๊ณ  LLM ์ปจํ…์ŠคํŠธ๋ฅผ ๋‚ญ๋น„ํ•ฉ๋‹ˆ๋‹ค
  • 500ํ† ํฐ์€ ํ•œ ๊ฐ€์ง€ ๊ฐœ๋…์„ ์„ค๋ช…ํ•˜๊ธฐ์— ์ถฉ๋ถ„ํ•˜๋ฉด์„œ๋„ ๊ฒ€์ƒ‰์— ๋‚ ์นด๋กœ์šด ์‚ฌ์ด์ฆˆ์ž…๋‹ˆ๋‹ค

์™œ 50ํ† ํฐ ์˜ค๋ฒ„๋žฉ์ธ๊ฐ€?

์˜ค๋ฒ„๋žฉ ์—†์ด ์ž๋ฅด๋ฉด ์ด๋Ÿฐ ์ผ์ด ์ƒ๊น๋‹ˆ๋‹ค:

[์ฒญํฌ 1] "...VLOOKUP์€ ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ๋งŒ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ,"
[์ฒญํฌ 2] "์ด ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๋ ค๋ฉด INDEX/MATCH ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค."

์ฒญํฌ 2๋งŒ ๊ฒ€์ƒ‰๋˜๋ฉด "๋ญ˜ ๊ทน๋ณตํ•œ๋‹ค๋Š” ๊ฑด์ง€" ๋ฌธ๋งฅ์„ ์•Œ ์ˆ˜ ์—†์ฃ . 50ํ† ํฐ ์˜ค๋ฒ„๋žฉ์ด ์žˆ์œผ๋ฉด:

[์ฒญํฌ 2] "...VLOOKUP์€ ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ๋งŒ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, 
          ์ด ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๋ ค๋ฉด INDEX/MATCH ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค."

์ด์ „ ์ฒญํฌ์˜ ๊ผฌ๋ฆฌ ๋ถ€๋ถ„์ด ๋‹ค์Œ ์ฒญํฌ ์‹œ์ž‘์— ํฌํ•จ๋˜์–ด ๋ฌธ๋งฅ์ด ์ด์–ด์ง‘๋‹ˆ๋‹ค.

๋ถ„ํ•  ์ „๋žต์€ 3๋‹จ๊ณ„๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค:

// 1์ฐจ: ๋ฌธ์„œ ๊ตฌ์กฐ(ํ—ค๋”ฉ, ์‹œํŠธ) ๊ฒฝ๊ณ„๋กœ ์„น์…˜ ๋ถ„๋ฆฌ
export function splitMarkdownIntoSections(content: string): DocumentSection[] {
  // ## ํ—ค๋”ฉ ๊ฒฝ๊ณ„๋กœ ๋ถ„ํ• 
  for (const line of lines) {
    if (line.match(/^#{1,3}\s+/)) {
      // ์ƒˆ๋กœ์šด ์„น์…˜ ์‹œ์ž‘
    }
  }
}

// 2์ฐจ: 500ํ† ํฐ ์ดˆ๊ณผ ์„น์…˜์€ ๋ฌธ๋‹จ(\\n\\n) ๋‹จ์œ„๋กœ ๋ถ„ํ•  + overlap
function splitWithOverlap(text: string, maxTokens: number, overlapTokens: number): string[] {
  const paragraphs = text.split(/\n\n+/);
  // ๋ฌธ๋‹จ ๊ฒฝ๊ณ„๋ฅผ ์กด์ค‘ํ•˜๋ฉฐ ๋ถ„ํ• , overlap ์ ์šฉ
}

// 3์ฐจ: ๋‹จ์ผ ๋ฌธ๋‹จ์ด 500ํ† ํฐ ์ดˆ๊ณผ ์‹œ ๋ฌธ์žฅ ๋‹จ์œ„๋กœ ๊ฐ•์ œ ๋ถ„ํ• 
function splitLongParagraph(text: string, maxTokens: number): string[] {
  const sentences = text.split(/(?<=[.!?ใ€‚๏ผ๏ผŸ])\s+/);
  // ๋ฌธ์žฅ ๊ฒฝ๊ณ„๋กœ ๋ถ„ํ• 
}

ํ† ํฐ ์ถ”์ • ํ•จ์ˆ˜๋„ ํฅ๋ฏธ๋กญ์Šต๋‹ˆ๋‹ค. ์ •ํ™•ํ•œ ํ† ํฌ๋‚˜์ด์ €(tiktoken ๋“ฑ) ๋Œ€์‹  ๊ฒฝํ—˜์  ๋น„์œจ์„ ์‚ฌ์šฉํ•ด ์˜์กด์„ฑ์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค:

export function estimateTokens(text: string): number {
  if (!text) return 0;
  const koreanChars = (text.match(/[\uAC00-\uD7AF]/g) || []).length;
  // ํ•œ๊ตญ์–ด ๋น„์œจ 30% ์ด์ƒ์ด๋ฉด 2 chars/token, ์•„๋‹ˆ๋ฉด 3 chars/token
  const ratio = koreanChars > text.length * 0.3 ? 2 : 3;
  return Math.ceil(text.length / ratio);
}

ํ•œ๊ตญ์–ด๋Š” ์˜์–ด๋ณด๋‹ค ํ† ํฐ ๋ฐ€๋„๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค(ํ•œ ๊ธ€์ž๊ฐ€ ๋Œ€๋žต 0.5ํ† ํฐ). ํ•œ์˜ ํ˜ผํ•ฉ ๋ฌธ์„œ๊ฐ€ ๋งŽ์€ OpenPersona ํŠน์„ฑ์„ ๋ฐ˜์˜ํ•œ ์ˆ˜์น˜์ž…๋‹ˆ๋‹ค.

3. Embedding โ€” Ports & Adapters ํŒจํ„ด

์ฒญํฌ๊ฐ€ ๋งŒ๋“ค์–ด์ง€๋ฉด ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. OpenPersona๋Š” Ports & Adapters(ํ—ฅ์‚ฌ๊ณ ๋‚ ) ํŒจํ„ด์œผ๋กœ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์„ ์ถ”์ƒํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค:

// ports/embedding.port.ts โ€” ์ธํ„ฐํŽ˜์ด์Šค(Port)
export interface EmbeddingPort {
  embed(text: string): Promise<number[]>;
  embedBatch(texts: string[]): Promise<number[][]>;
  readonly dimensions: number;
  readonly modelName: string;
}
// adapters/gemini-embedding.adapter.ts โ€” ๊ตฌํ˜„์ฒด(Adapter)
export class GeminiEmbeddingAdapter implements EmbeddingPort {
  readonly dimensions = 768;
  readonly modelName = 'gemini-embedding-001';
  // ...
}

// adapters/openai-embedding.adapter.ts
export class OpenAIEmbeddingAdapter implements EmbeddingPort {
  readonly dimensions = 1536;
  readonly modelName = 'text-embedding-3-small';
  // ...
}

Gemini ์šฐ์„ , OpenAI ํด๋ฐฑ ์ „๋žต์ž…๋‹ˆ๋‹ค. Gemini Embedding์€ ๋ฌด๋ฃŒ ํ‹ฐ์–ด๊ฐ€ ์žˆ์–ด ๋น„์šฉ ํšจ์œจ์ ์ด๊ณ , API ํ‚ค๊ฐ€ ์—†์„ ๋•Œ๋งŒ OpenAI๋กœ ์ž๋™ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.

์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์ด ๋ฐ”๋€Œ๋ฉด ๊ธฐ์กด ๋ฒกํ„ฐ์™€ ํ˜ธํ™˜๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋ชจ๋ธ ๋ณ€๊ฒฝ ๊ฐ์ง€ โ†’ ์ธ๋ฑ์Šค ์ž๋™ ์žฌ์ƒ์„ฑ ๋กœ์ง๋„ ํฌํ•จํ–ˆ์Šต๋‹ˆ๋‹ค:

// rag-engine.ts
async ensureEmbeddingConsistency(): Promise<boolean> {
  const currentModel = `${this.embedding.modelName}:${this.embedding.dimensions}`;
  const stored = await fs.readFile(markerPath, 'utf-8');
  
  if (stored !== currentModel) {
    console.log(`์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ๋ณ€๊ฒฝ ๊ฐ์ง€: ${stored} โ†’ ${currentModel}, ์ธ๋ฑ์Šค ์žฌ์ƒ์„ฑ`);
    await this.clearAllIndexes();
    return true;
  }
  return false;
}

Vector Store๋„ ๋™์ผํ•˜๊ฒŒ ์ถ”์ƒํ™”๋˜์–ด ์žˆ์–ด, ํ˜„์žฌ Vectra(๋กœ์ปฌ ํŒŒ์ผ ๊ธฐ๋ฐ˜)์—์„œ ๋‚˜์ค‘์— LanceDB๋‚˜ ChromaDB๋กœ ๊ต์ฒดํ•  ๋•Œ ์–ด๋Œ‘ํ„ฐ ํ•˜๋‚˜๋งŒ ๋ฐ”๊พธ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

4. Hybrid Search โ€” "๋†“์น˜์ง€ ์•Š๋Š”" ๊ฒ€์ƒ‰

Hybrid Search + RRF Merge + LLM Reranking ํŒŒ์ดํ”„๋ผ์ธ โ€” ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰๊ณผ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์„ RRF๋กœ ๋ณ‘ํ•ฉํ•˜๊ณ  LLM์ด ์ตœ์ข… ๋ฆฌ๋žญํ‚น

์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰์„ ์™œ ๋‘ ๊ฐ€์ง€๋กœ ํ•˜๋Š” ๊ฑธ๊นŒ์š”?

๐Ÿซ ๋„์„œ๊ด€ ๋น„์œ ๋กœ ์ดํ•ดํ•˜๊ธฐ

๋„์„œ๊ด€์—์„œ "VLOOKUP ํ•จ์ˆ˜"์— ๋Œ€ํ•œ ์ฑ…์„ ์ฐพ๋Š”๋‹ค๊ณ  ํ•ด๋ด…์‹œ๋‹ค.

์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰(๋ฒกํ„ฐ) = ์ฃผ์ œ๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒƒ

  • "์—‘์…€์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์•„์˜ค๋Š” ๋ฐฉ๋ฒ•"์ด๋ผ๋Š” ์˜๋ฏธ๋กœ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค
  • "VLOOKUP"์ด๋ž€ ๋‹จ์–ด๊ฐ€ ์—†์–ด๋„, "ํ‘œ์—์„œ ๊ฐ’์„ ์กฐํšŒํ•˜๋Š” ํ•จ์ˆ˜"๋ผ๋Š” ๋œป์ด ๋น„์Šทํ•˜๋ฉด ์ฐพ์•„๋ƒ…๋‹ˆ๋‹ค
  • ์žฅ์ : ๋™์˜์–ด, ์œ ์‚ฌ ํ‘œํ˜„์„ ์ดํ•ดํ•ฉ๋‹ˆ๋‹ค
  • ๋‹จ์ : "VLOOKUP"์ด๋ผ๋Š” ์ •ํ™•ํ•œ ์šฉ์–ด๋ฅผ ์ค‘์š”ํ•˜๊ฒŒ ์ทจ๊ธ‰ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰(BM25) = ์ œ๋ชฉ/๋ชฉ์ฐจ์—์„œ ๋‹จ์–ด๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒƒ

  • "VLOOKUP"์ด๋ผ๋Š” ์ •ํ™•ํ•œ ๊ธ€์ž๊ฐ€ ๋“ค์–ด๊ฐ„ ๋ฌธ์„œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค
  • ์žฅ์ : ๊ณ ์œ  ๋ช…์‚ฌ, ํ•จ์ˆ˜๋ช…, ์•ฝ์–ด๋ฅผ ์ •ํ™•ํžˆ ๋งค์นญํ•ฉ๋‹ˆ๋‹ค
  • ๋‹จ์ : "ํ‘œ ์กฐํšŒ ํ•จ์ˆ˜"๋ผ๊ณ  ์จ์žˆ์œผ๋ฉด ๋ชป ์ฐพ์Šต๋‹ˆ๋‹ค

Hybrid Search = ๋‘ ์‚ฌ์„œ์—๊ฒŒ ๋™์‹œ์— ๋ถ€ํƒํ•˜๋Š” ๊ฒƒ

  • ์ฃผ์ œ ์ „๋ฌธ๊ฐ€ ์‚ฌ์„œ + ์ƒ‰์ธ ์ „๋ฌธ๊ฐ€ ์‚ฌ์„œ๊ฐ€ ๊ฐ๊ฐ ์ฐพ์•„์™€์„œ ํ•ฉ์นฉ๋‹ˆ๋‹ค
  • ์˜๋ฏธ๋„ ๋งž๊ณ , ์ •ํ™•ํ•œ ์šฉ์–ด๋„ ๋“ค์–ด์žˆ๋Š” ๋ฌธ์„œ๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ์ ์ˆ˜๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค

RAG Engine์˜ search ๋ฉ”์„œ๋“œ๋ฅผ ๋ณด๋ฉด ์ด ์ „๋žต์ด ์ฝ”๋“œ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

// rag-engine.ts
async search(request: RAGSearchRequest): Promise<SearchResult[]> {
  const { query, characterId, category, topK = 5, useReranking = true } = request;
  const queryVector = await this.embedding.embed(query);

  // 1) ๋ฒกํ„ฐ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ โ€” static + learned ์ธ๋ฑ์Šค ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰
  const [staticResults, learnedResults] = await Promise.all([
    this.queryStore(characterId, 'static', queryVector, filter, topK * 2),
    this.queryStore(characterId, 'learned', queryVector, filter, topK * 2),
  ]);
  const allSemanticResults = [...staticResults, ...learnedResults]
    .sort((a, b) => b.score - a.score)
    .slice(0, topK * 2);

  // 2) BM25 ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰
  const allChunks = await this.getAllChunks(characterId, category);
  const keywordResults = keywordSearch(query, allChunks, topK * 2);

  // 3) RRF ๋ณ‘ํ•ฉ
  const merged = mergeWithRRF(allSemanticResults, keywordResults, topK * 2);

  // 4) LLM ๋ฆฌ๋žญํ‚น
  if (useReranking && merged.length > topK) {
    return rerankWithLLM(query, merged, topK, this.config.quickLLMCall);
  }
  return merged.slice(0, topK);
}

topK * 2๋กœ ๋„‰๋„‰ํžˆ ๊ฐ€์ ธ์˜จ ํ›„ ๋ณ‘ํ•ฉ/๋ฆฌ๋žญํ‚น์œผ๋กœ ์ค„์—ฌ๊ฐ€๋Š” ํผ๋„(funnel) ์ „๋žต์ž…๋‹ˆ๋‹ค. ๋†“์น˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋งŽ์ด ๊ฐ€์ ธ์™€์„œ ์ •์ œํ•˜๋Š” ๊ฒŒ ๋‚ซ์Šต๋‹ˆ๋‹ค.

BM25 ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์˜ ๊ตฌํ˜„๋„ ์‚ดํŽด๋ด…์‹œ๋‹ค:

// keyword-search.ts
function calculateBM25Score(
  queryTerms: string[], document: string, totalDocs: number,
): number {
  const docTerms = tokenize(document);
  const k1 = 1.2;   // ์šฉ์–ด ๋นˆ๋„ ํฌํ™” ๊ณ„์ˆ˜
  const b = 0.75;    // ๋ฌธ์„œ ๊ธธ์ด ์ •๊ทœํ™” ๊ณ„์ˆ˜

  for (const queryTerm of queryTerms) {
    const tf = termFreq.get(queryTerm) ?? 0;
    if (tf === 0) continue;
    const idf = Math.log(1 + (totalDocs - 1) / (1 + 1));
    const tfNorm = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLen / avgDocLen)));
    score += idf * tfNorm;
  }
  return score;
}

k1 = 1.2๋Š” ๊ฐ™์€ ๋‹จ์–ด๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜์™€๋„ ์ ์ˆ˜๊ฐ€ ๋ฌดํ•œํžˆ ์˜ฌ๋ผ๊ฐ€์ง€ ์•Š๊ฒŒ ํ•˜๊ณ , b = 0.75๋Š” ๊ธด ๋ฌธ์„œ๊ฐ€ ๋ถˆ๊ณต์ •ํ•˜๊ฒŒ ์œ ๋ฆฌํ•ด์ง€์ง€ ์•Š๋„๋ก ๋ณด์ •ํ•ฉ๋‹ˆ๋‹ค. ์ •๋ณด ๊ฒ€์ƒ‰ ๋ถ„์•ผ์—์„œ ์ˆ˜์‹ญ ๋…„๊ฐ„ ๊ฒ€์ฆ๋œ ํŒŒ๋ผ๋ฏธํ„ฐ์ž…๋‹ˆ๋‹ค.

5. RRF Merge โ€” ๋‘ ์ „๋ฌธ๊ฐ€์˜ ์ถ”์ฒœ์„ ํ•ฉ์น˜๋Š” ํˆฌํ‘œ

์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰๊ณผ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰, ๋‘ ๊ฐ€์ง€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™”์Šต๋‹ˆ๋‹ค. ์ด๊ฑธ ์–ด๋–ป๊ฒŒ ํ•ฉ์น ๊นŒ์š”?

๐Ÿ—ณ๏ธ ํˆฌํ‘œ ๋น„์œ ๋กœ ์ดํ•ดํ•˜๊ธฐ

๋‘ ๋ช…์˜ ์˜ํ™” ํ‰๋ก ๊ฐ€๊ฐ€ ๊ฐ๊ฐ "์ด๋ฒˆ ๋‹ฌ ์ถ”์ฒœ ์˜ํ™” Top 10"์„ ์คฌ๋‹ค๊ณ  ํ•ด๋ด…์‹œ๋‹ค.

  • A ํ‰๋ก ๊ฐ€(์‹œ๋งจํ‹ฑ): "1์œ„ ์ธ์…‰์…˜, 2์œ„ ์ธํ„ฐ์Šคํ…”๋ผ, 3์œ„ ๋งคํŠธ๋ฆญ์Šค..."
  • B ํ‰๋ก ๊ฐ€(ํ‚ค์›Œ๋“œ): "1์œ„ ๋งคํŠธ๋ฆญ์Šค, 2์œ„ ์ธ์…‰์…˜, 3์œ„ ๋ธ”๋ ˆ์ด๋“œ ๋Ÿฌ๋„ˆ..."

๋‹จ์ˆœํžˆ ์ ์ˆ˜๋ฅผ ๋”ํ•˜๋ฉด? A์˜ ์ ์ˆ˜ ์ฒด๊ณ„(01)์™€ B์˜ ์ ์ˆ˜ ์ฒด๊ณ„(050)๊ฐ€ ๋‹ฌ๋ผ์„œ ๋ถˆ๊ณต์ •ํ•ฉ๋‹ˆ๋‹ค.

RRF๋Š” "์ˆœ์œ„"๋งŒ ๋ณด๊ณ  ํ•ฉ์นฉ๋‹ˆ๋‹ค:

  • ์ธ์…‰์…˜: A์—์„œ 1์œ„(๋†’์€ ์ ์ˆ˜) + B์—์„œ 2์œ„(๋†’์€ ์ ์ˆ˜) = ์ตœ์ข… 1์œ„
  • ๋งคํŠธ๋ฆญ์Šค: A์—์„œ 3์œ„ + B์—์„œ 1์œ„ = ์ตœ์ข… 2์œ„
  • ๋‘˜ ๋‹ค ๋†’์ด ํ‰๊ฐ€ํ•œ ํ•ญ๋ชฉ์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์˜ฌ๋ผ์˜ต๋‹ˆ๋‹ค

์ฝ”๋“œ๋กœ ๋ณด๋ฉด ๋†€๋ž๋„๋ก ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค:

// keyword-search.ts
export function mergeWithRRF(
  semanticResults: SearchResult[],
  keywordResults: SearchResult[],
  topK: number,
  k: number = 60,  // RRF ์ƒ์ˆ˜ (๋…ผ๋ฌธ ๊ถŒ์žฅ๊ฐ’)
): SearchResult[] {
  const scoreMap = new Map<string, { result: SearchResult; rrfScore: number }>();

  // ์‹œ๋งจํ‹ฑ ๊ฒฐ๊ณผ์˜ ์ˆœ์œ„๋กœ ์ ์ˆ˜ ๊ณ„์‚ฐ
  for (let rank = 0; rank < semanticResults.length; rank++) {
    const r = semanticResults[rank];
    const rrfScore = 1 / (k + rank + 1);
    scoreMap.set(r.id, { result: r, rrfScore });
  }

  // ํ‚ค์›Œ๋“œ ๊ฒฐ๊ณผ์˜ ์ˆœ์œ„๋กœ ์ ์ˆ˜๋ฅผ ๋ˆ„์ 
  for (let rank = 0; rank < keywordResults.length; rank++) {
    const r = keywordResults[rank];
    const rrfScore = 1 / (k + rank + 1);
    const existing = scoreMap.get(r.id);
    if (existing) {
      existing.rrfScore += rrfScore;  // ์–‘์ชฝ ๋‹ค ์žˆ์œผ๋ฉด ์ ์ˆ˜ ํ•ฉ์‚ฐ!
    } else {
      scoreMap.set(r.id, { result: r, rrfScore });
    }
  }

  return [...scoreMap.values()]
    .sort((a, b) => b.rrfScore - a.rrfScore)
    .slice(0, topK);
}

k = 60์€ Cormack et al. (2009) ๋…ผ๋ฌธ์—์„œ ์ œ์•ˆํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ’์ด ํด์ˆ˜๋ก ์ˆœ์œ„ ์ฐจ์ด์— ๋”ฐ๋ฅธ ์ ์ˆ˜ ์ฐจ์ด๊ฐ€ ์ค„์–ด๋“ค์–ด, ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๊ณจ๊ณ ๋ฃจ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.

6. LLM Reranking โ€” ์ตœ์ข… ๋ฉด์ ‘๊ด€์˜ ํŒ๋‹จ

RRF๋กœ 10๊ฐœ๋ฅผ ๊ณจ๋ž์ง€๋งŒ, ์ตœ์ข…์ ์œผ๋กœ LLM์— ์ „๋‹ฌํ•  ๊ฑด 5๊ฐœ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ LLM์ด ์ง์ ‘ ์ฝ์–ด๋ณด๊ณ  ๊ด€๋ จ์„ฑ ์ˆœ์œ„๋ฅผ ๋‹ค์‹œ ๋งค๊น๋‹ˆ๋‹ค.

๐ŸŽฏ ๋ฉด์ ‘ ๋น„์œ ๋กœ ์ดํ•ดํ•˜๊ธฐ

์„œ๋ฅ˜ ์ „ํ˜•(์‹œ๋งจํ‹ฑ + ํ‚ค์›Œ๋“œ)์œผ๋กœ 10๋ช…์„ ํ†ต๊ณผ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. ์ด์ œ **์ตœ์ข… ๋ฉด์ ‘๊ด€(Gemini Flash)**์ด ๊ฐ ํ›„๋ณด์˜ ์ด๋ ฅ์„œ๋ฅผ ์ง์ ‘ ์ฝ์–ด๋ณด๊ณ , ์ด ์ง๋ฌด์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ 5๋ช…์„ ๋ฝ‘์Šต๋‹ˆ๋‹ค.

์„œ๋ฅ˜ ์ „ํ˜•์€ ํ‚ค์›Œ๋“œ์™€ ๊ฒฝ๋ ฅ ๋งค์นญ์œผ๋กœ ๋น ๋ฅด๊ฒŒ ๊ฑฐ๋ฅด์ง€๋งŒ, ๋ฉด์ ‘๊ด€์€ "์ด ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์œผ๋กœ ์ด ๋‚ด์šฉ์ด ์ •๋ง ์œ ์šฉํ•œ๊ฐ€?"๋ฅผ ๋งฅ๋ฝ์„ ์ดํ•ดํ•˜๋ฉด์„œ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// reranker.ts
export async function rerankWithLLM(
  query: string, results: SearchResult[], topK: number, llmCall: QuickLLMCall,
): Promise<SearchResult[]> {
  if (results.length <= topK) return results;

  // ๊ฐ ๊ฒฐ๊ณผ์˜ ์•ž 150์ž๋งŒ ์ž˜๋ผ์„œ LLM์— ์ „๋‹ฌ (๋น„์šฉ ์ ˆ์•ฝ)
  const snippets = results
    .map((r, i) => `[${i}] ${r.content.slice(0, 150).replace(/\n/g, ' ')}`)
    .join('\n');

  const prompt = [
    '๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ ์งˆ๋ฌธ๊ณผ ๊ฐ€์žฅ ๊ด€๋ จ ๋†’์€ ๊ฒƒ์„ ์„ ํƒํ•˜์„ธ์š”.',
    '',
    `์งˆ๋ฌธ: "${query}"`,
    '',
    '๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ:',
    snippets,
    '',
    `๊ฐ€์žฅ ๊ด€๋ จ ๋†’์€ ${topK}๊ฐœ์˜ ์ธ๋ฑ์Šค๋ฅผ JSON ๋ฐฐ์—ด๋กœ๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ์˜ˆ: [0, 3, 1]`,
  ].join('\n');

  const response = await llmCall(prompt);
  const indices = parseIndices(response, results.length);
  return indices.slice(0, topK).map((i) => results[i]);
}

๋น„์šฉ์€ ์–ผ๋งˆ๋‚˜ ๋“ค๊นŒ์š”? ํ”„๋กฌํ”„ํŠธ๊ฐ€ ~100ํ† ํฐ ์ˆ˜์ค€์ด๋ผ Gemini Flash ๊ธฐ์ค€์œผ๋กœ ์‚ฌ์‹ค์ƒ ๋ฌด๋ฃŒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์€ ๋ˆˆ์— ๋„๊ฒŒ ์˜ฌ๋ผ๊ฐ‘๋‹ˆ๋‹ค. ํŠนํžˆ "VLOOKUP ์—๋Ÿฌ ํ•ด๊ฒฐ"์„ ๋ฌผ์–ด๋ดค๋Š”๋ฐ "VLOOKUP ๊ธฐ๋ณธ ๋ฌธ๋ฒ•"๊ณผ "VLOOKUP ์—๋Ÿฌ ํ•ธ๋“ค๋ง"์ด ๋‘˜ ๋‹ค ๊ฒ€์ƒ‰๋์„ ๋•Œ, LLM์ด ํ›„์ž๋ฅผ ๋” ๋†’์€ ์ˆœ์œ„๋กœ ์˜ฌ๋ ค์ค๋‹ˆ๋‹ค.

๋ฆฌ๋žญํ‚น์ด ์‹คํŒจํ•ด๋„ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค โ€” catch ๋ธ”๋ก์—์„œ ์›๋ณธ ์ˆœ์„œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” graceful degradation ํŒจํ„ด์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

7. ํ† ํฐ ์ตœ์ ํ™” ์ „๋žต

RAG๊ฐ€ ์•„๋ฌด๋ฆฌ ์ข‹์€ ์ปจํ…์ŠคํŠธ๋ฅผ ์ฐพ์•„์™€๋„, LLM์˜ ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์†Œ์šฉ์—†์Šต๋‹ˆ๋‹ค. ๋ช‡ ๊ฐ€์ง€ ํ† ํฐ ์ ˆ์•ฝ ์ „๋žต์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค:

RAG ์ปจํ…์ŠคํŠธ ์ตœ๋Œ€ 8,000์ž ์ œํ•œ

// context-builder.ts
const MAX_RAG_CONTEXT_CHARS = 8000;

function buildSystemPrompt(basePrompt: string, ragContext: string): string {
  const trimmedContext = ragContext.length > MAX_RAG_CONTEXT_CHARS
    ? ragContext.slice(0, MAX_RAG_CONTEXT_CHARS) + '\n...(truncated)'
    : ragContext;
  // ...
}

๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ 20ํ„ด ์ œํ•œ โ€” Context Builder์—์„œ ์ตœ๊ทผ 20ํ„ด๋งŒ ํฌํ•จํ•˜์—ฌ ํ† ํฐ ํญ๋ฐœ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ๋žญํ‚น ์‹œ 150์ž๋งŒ ์ „๋‹ฌ โ€” ์ „์ฒด ์ฒญํฌ๋ฅผ LLM์— ๋ณด๋‚ด๋ฉด ๋น„์šฉ์ด ์˜ฌ๋ผ๊ฐ€๋ฏ€๋กœ, ์•ž 150์ž๋งŒ ์ž˜๋ผ์„œ ํŒ๋‹จํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๋ฌธ์„œ๋Š” ์•ž๋ถ€๋ถ„์— ํ•ต์‹ฌ ์ •๋ณด๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.


๐ŸŽฏ Orchestrator ์•„ํ‚คํ…์ฒ˜

RAG๊ฐ€ "์ง€์‹์„ ๊ฒ€์ƒ‰ํ•˜๋Š” ์—”์ง„"์ด๋ผ๋ฉด, Orchestrator๋Š” **"๋ชจ๋“  ๊ฒƒ์„ ์กฐ์œจํ•˜๋Š” ์ง€ํœ˜์ž"**์ž…๋‹ˆ๋‹ค.

Intent Classifier โ€” ์˜๋„ ํŒŒ์•…

์‚ฌ์šฉ์ž๊ฐ€ ๋ญ˜ ์›ํ•˜๋Š”์ง€ ๋ชจ๋ฅด๋ฉด ์•„๋ฌด๊ฒƒ๋„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Intent Classifier๋Š” ํ‚ค์›Œ๋“œ ํŒจํ„ด ๋งค์นญ์œผ๋กœ ๋น ๋ฅด๊ฒŒ ์˜๋„๋ฅผ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค:

// intent-classifier.ts
const KEYWORD_RULES = [
  // ํŒŒ์ผ ์กฐ์ž‘
  { pattern: /ํŒŒ์ผ\s*(์ฝ|์“ฐ|์ƒ์„ฑ|์‚ญ์ œ|๋ชฉ๋ก|์—ด์–ด|๋งŒ๋“ค์–ด)/i, 
    type: 'file_operation', needsKnowledge: false, needsTool: true },
  // ๋ฒˆ์—ญ
  { pattern: /๋ฒˆ์—ญ|translate|็ฟป่จณ|ํ†ต์—ญ/i, 
    type: 'translation', needsKnowledge: true, needsTool: false },
  // ์—‘์…€
  { pattern: /์—‘์…€|excel|์Šคํ”„๋ ˆ๋“œ์‹œํŠธ/i, 
    type: 'document_generation', needsKnowledge: true, needsTool: true },
  // ...10+ ํŒจํ„ด
];

์บ๋ฆญํ„ฐ๋ณ„ ์ „๋ฌธ ๋ถ„์•ผ๋„ ๋งคํ•‘๋˜์–ด ์žˆ์–ด์„œ, Fox์—๊ฒŒ ๋ฌผ์œผ๋ฉด code_review, code_generation ์˜๋„๊ฐ€ ์šฐ์„  ๊ณ ๋ ค๋˜๊ณ , Rabbit์—๊ฒŒ ๋ฌผ์œผ๋ฉด translation์ด ๊ธฐ๋ณธ ์˜๋„๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ RAG๊ฐ€ ํ•„์š”ํ•œ์ง€(needsKnowledge), ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ•œ์ง€(needsTool)๊ฐ€ ๊ฒฐ์ •๋˜์–ด, ๋ถˆํ•„์š”ํ•œ RAG ๊ฒ€์ƒ‰์ด๋‚˜ ๋„๊ตฌ ๋กœ๋”ฉ์„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ ์žก๋‹ด์—๋Š” RAG ์—†์ด ๋ฐ”๋กœ LLM์— ์ „๋‹ฌํ•˜๋Š” ์‹์ด์ฃ .

Smart Model Selector + Auto-Fallback

์˜๋„์— ๋”ฐ๋ผ ์ตœ์ ์˜ ๋ชจ๋ธ์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ „๋žต์€ Gemini Flash ์šฐ์„  (๋น ๋ฅด๊ณ , ๋ฌด๋ฃŒ ํ‹ฐ์–ด)์ด๋ฉฐ, Gemini API๊ฐ€ quota ์ดˆ๊ณผ ๋“ฑ์œผ๋กœ ์‹คํŒจํ•˜๋ฉด OpenAI๋กœ ์ž๋™ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค:

// orchestrator.ts
const FALLBACK_MODELS: Record<string, { provider: string; model: string }> = {
  openai: { provider: 'gemini', model: 'gemini-2.0-flash' },
  gemini: { provider: 'openai', model: 'gpt-4o-mini' },
};

// ์„ ํƒ๋œ Provider ์‹คํŒจ ์‹œ ์ž๋™ ํด๋ฐฑ
try {
  yield* this.chatWithToolLoop(modelSelection.provider, modelSelection.model, messages, toolDefs);
} catch (error) {
  if (isQuotaOrAuthError(error)) {
    const fallback = FALLBACK_MODELS[modelSelection.provider];
    yield* this.chatWithToolLoop(fallback.provider, fallback.model, messages, toolDefs);
  }
}

์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” ๋ชจ๋ธ์ด ๋ฐ”๋€Œ์—ˆ๋Š”์ง€ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ๊ทธ๋ƒฅ ๋‹ต๋ณ€์ด ๋‚˜์˜ฌ ๋ฟ์ด์ฃ . ์ด๊ฒƒ์ด **๊ฐ€์šฉ์„ฑ(availability)**์„ ๋ณด์žฅํ•˜๋Š” ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

Tool Call Loop โ€” ๋„๊ตฌ๋ฅผ ์“ฐ๋Š” ์—์ด์ „ํŠธ

"ํŒŒ์ผ ์ฝ์–ด์ค˜", "์—‘์…€ ๋งŒ๋“ค์–ด์ค˜" ๊ฐ™์€ ์š”์ฒญ์€ LLM ํ˜ผ์ž ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Orchestrator๋Š” ์ตœ๋Œ€ 5๋ผ์šด๋“œ๊นŒ์ง€ LLM โ†” ๋„๊ตฌ ์‹คํ–‰์„ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค:

// orchestrator.ts
private async *chatWithToolLoop(
  provider: string, model: string, messages: MessageInput[], tools?: ToolDefinition[],
): AsyncGenerator<StreamChunk> {
  let round = 0;

  while (round < MAX_TOOL_ROUNDS) {  // ์ตœ๋Œ€ 5๋ผ์šด๋“œ
    for await (const chunk of this.router.chatWith(provider, model, currentMessages, tools)) {
      if (chunk.toolCall) {
        pendingToolCalls.push(chunk.toolCall);
      }
      // ํ…์ŠคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ...
    }

    if (pendingToolCalls.length === 0) break;  // ๋„๊ตฌ ํ˜ธ์ถœ ์—†์œผ๋ฉด ์ข…๋ฃŒ

    // ๋„๊ตฌ ์‹คํ–‰ โ†’ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”์‹œ์ง€์— ์ถ”๊ฐ€ โ†’ ๋‹ค์Œ ๋ผ์šด๋“œ
    for (const tc of pendingToolCalls) {
      const result = await this.toolRegistry.execute(tc);
      currentMessages.push({
        role: 'tool',
        content: result.success ? result.output : `Error: ${result.error}`,
        toolCallId: tc.id,
      });
    }
    round++;
  }
}

์˜ˆ๋ฅผ ๋“ค์–ด "ํ”„๋กœ์ ํŠธ์˜ package.json ์ฝ์–ด์ค˜"๋ผ๊ณ  ํ•˜๋ฉด:

  1. LLM์ด readFile ๋„๊ตฌ ํ˜ธ์ถœ์„ ์ƒ์„ฑ
  2. Tool Registry๊ฐ€ ์‹ค์ œ ํŒŒ์ผ์„ ์ฝ์Œ
  3. ๊ฒฐ๊ณผ๋ฅผ LLM์— ๋Œ๋ ค์คŒ
  4. LLM์ด ๋‚ด์šฉ์„ ๋ถ„์„ํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋‹ต๋ณ€

์ด ๋ฃจํ”„๊ฐ€ ์ตœ๋Œ€ 5๋ฒˆ ๋ฐ˜๋ณต๋˜๋ฏ€๋กœ, "ํŒŒ์ผ์„ ์ฝ๊ณ  โ†’ ๋ถ„์„ํ•˜๊ณ  โ†’ ๊ฒฐ๊ณผ๋ฅผ ์—‘์…€๋กœ ์ €์žฅํ•ด์ค˜" ๊ฐ™์€ ๋ณตํ•ฉ ์ž‘์—…๋„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ“– Knowledge Base & Learning System

์บ๋ฆญํ„ฐ๋ณ„ 16์ข…, 30๊ฐœ ํŒŒ์ผ์˜ ์ •์  ์ง€์‹์ด ์•ฑ ์‹œ์ž‘ ์‹œ Vectra ์ธ๋ฑ์Šค์— ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค:

์บ๋ฆญํ„ฐ ์ง€์‹ ์˜์—ญ ํŒŒ์ผ ์ˆ˜
Fox (๊ฐœ๋ฐœ) Next.js, React Hooks, TypeScript, Figma Guide, Code Review, QA 13์ข…
Pig (๋ฌธ์„œ) Excel 6์ข…, PowerPoint 3์ข…, Word 1์ข…, HWP 3์ข… 13์ข…
Rabbit (๋ฒˆ์—ญ) ํ•œโ†”์˜, ํ•œโ†”์ผ, ์˜โ†”ํ•œ ๋ฒˆ์—ญ ํŒจํ„ด + ํ†ค ๊ฐ€์ด๋“œ 4์ข…

์ •์  ์ง€์‹ ์™ธ์—๋„ Learning Manager๊ฐ€ ์„ธ ๊ฐ€์ง€ ๊ฒฝ๋กœ๋กœ ์ง€์† ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค:

  • ๋Œ€ํ™” ํ•™์Šต: ์–‘์งˆ์˜ Q&A ์Œ์„ ์ž๋™ ์ถ”์ถœํ•˜์—ฌ learned ์ธ๋ฑ์Šค์— ์ €์žฅ
  • ํ”ผ๋“œ๋ฐฑ ํ•™์Šต: ์‚ฌ์šฉ์ž๊ฐ€ "correction" ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ๋ฉด ์ˆ˜์ •๋œ ๋‚ด์šฉ์„ ํ•™์Šต
  • ์ง€์‹ ์—…๋กœ๋“œ: .xlsx, .docx, .md ๋“ฑ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๋ฉด ํŒŒ์‹ฑ/์ฒญํ‚น ํ›„ ํ•™์Šต

static(์•ฑ ๋ฒˆ๋“ค)๊ณผ learned(์‚ฌ์šฉ์ž ํ•™์Šต) ์ธ๋ฑ์Šค๋ฅผ ๋ถ„๋ฆฌํ•œ ๋•๋ถ„์—, ํ•™์Šต ๋ฐ์ดํ„ฐ๊ฐ€ ์›๋ณธ ์ง€์‹์„ ์˜ค์—ผ์‹œํ‚ค์ง€ ์•Š์œผ๋ฉด์„œ๋„ ๊ฒ€์ƒ‰ ์‹œ์—๋Š” ๋‘˜ ๋‹ค ์ฐธ์กฐ๋ฉ๋‹ˆ๋‹ค.


๐ŸŽ‰ ๋งˆ๋ฌด๋ฆฌ

v1์—์„œ "์บ๋ฆญํ„ฐ๊ฐ€ ๋Œ€ํ™”ํ•˜๋Š” ์ฑ—๋ด‡"์ด์—ˆ๋˜ OpenPersona๊ฐ€, v2์—์„œ๋Š” ์ง€์‹์„ ๊ฒ€์ƒ‰ํ•˜๊ณ , ์˜๋„๋ฅผ ํŒŒ์•…ํ•˜๊ณ , ๋„๊ตฌ๋ฅผ ์‹คํ–‰ํ•˜๋Š” AI ์—์ด์ „ํŠธ๋กœ ์ง„ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ต์‹ฌ์€ ์„ธ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค:

  1. Hybrid Search + RRF + Reranking: ์˜๋ฏธ ๊ฒ€์ƒ‰๊ณผ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์„ ํ•ฉ์น˜๊ณ , LLM์ด ์ตœ์ข… ํŒ๋‹จํ•˜๋Š” 3๋‹จ๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์„ ๋Œ์–ด์˜ฌ๋ ธ์Šต๋‹ˆ๋‹ค.
  2. Structure-Aware Chunking: ๋ฌธ์„œ ๊ตฌ์กฐ๋ฅผ ์กด์ค‘ํ•˜๋ฉด์„œ 500ํ† ํฐ/50 overlap์œผ๋กœ ๋ถ„ํ• ํ•˜์—ฌ, ๊ฒ€์ƒ‰ ์ •ํ™•๋„์™€ ๋ฌธ๋งฅ ์—ฐ์†์„ฑ์„ ๋™์‹œ์— ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.
  3. Orchestrator: Intent ๋ถ„๋ฅ˜๋ถ€ํ„ฐ Model ์„ ํƒ, Tool Call Loop, Auto-Fallback๊นŒ์ง€ โ€” ์‚ฌ์šฉ์ž๋Š” ๊ทธ๋ƒฅ ์งˆ๋ฌธ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”— ๊ด€๋ จ ๋งํฌ

๐Ÿท๏ธ ํƒœ๊ทธ

#rag #hybrid_search #reranking #orchestration #ai_agent #electron #typescript #gemini #vector_search #bm25