๐ง OpenPersona v2.0 โ RAG ํ์ดํ๋ผ์ธ๊ณผ AI ์ค์ผ์คํธ๋ ์ด์ ๊ตฌ์ถ๊ธฐ
OpenPersona v2.0์์ Vectra ๊ธฐ๋ฐ ํ์ด๋ธ๋ฆฌ๋ ๊ฒ์(๋ฒกํฐ + BM25), RRF ๋ณํฉ, LLM ๋ฆฌ๋ญํน์ผ๋ก ๊ตฌ์ฑ๋ RAG ํ์ดํ๋ผ์ธ๊ณผ Intent ๋ถ๋ฅ โ Model ์ ํ โ Tool Call Loop๊น์ง AI Agent Orchestrator๋ฅผ ์ค์ ์ฝ๋์ ํจ๊ป ์์ธํ ๋ค๋ฃน๋๋ค.

๐ ๊ธ ๊ฐ์
์ด์ ๊ธ์์ 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๊ฐ ์ค์ ํ๋ธ๊ฐ ๋์ด ๋ชจ๋ ์์ฒญ์ ์กฐ์จํฉ๋๋ค.
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 โ ๋๋ํ ๋ฌธ์ ๋ถํ
๋ฌธ์๋ฅผ ํต์งธ๋ก ์๋ฒ ๋ฉํ ์๋ ์์ต๋๋ค. 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 โ "๋์น์ง ์๋" ๊ฒ์
์ฌ๊ธฐ์๋ถํฐ ํต์ฌ์ ๋๋ค. ๊ฒ์์ ์ ๋ ๊ฐ์ง๋ก ํ๋ ๊ฑธ๊น์?
๐ซ ๋์๊ด ๋น์ ๋ก ์ดํดํ๊ธฐ
๋์๊ด์์ "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์ ์ ์ ์ฒด๊ณ(0
1)์ 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 ์ฝ์ด์ค"๋ผ๊ณ ํ๋ฉด:
- LLM์ด
readFile๋๊ตฌ ํธ์ถ์ ์์ฑ - Tool Registry๊ฐ ์ค์ ํ์ผ์ ์ฝ์
- ๊ฒฐ๊ณผ๋ฅผ LLM์ ๋๋ ค์ค
- 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 ์์ด์ ํธ๋ก ์งํํ์ต๋๋ค. ํต์ฌ์ ์ธ ๊ฐ์ง์ ๋๋ค:
- Hybrid Search + RRF + Reranking: ์๋ฏธ ๊ฒ์๊ณผ ํค์๋ ๊ฒ์์ ํฉ์น๊ณ , LLM์ด ์ต์ข ํ๋จํ๋ 3๋จ๊ณ ํ์ดํ๋ผ์ธ์ผ๋ก ๊ฒ์ ํ์ง์ ๋์ด์ฌ๋ ธ์ต๋๋ค.
- Structure-Aware Chunking: ๋ฌธ์ ๊ตฌ์กฐ๋ฅผ ์กด์คํ๋ฉด์ 500ํ ํฐ/50 overlap์ผ๋ก ๋ถํ ํ์ฌ, ๊ฒ์ ์ ํ๋์ ๋ฌธ๋งฅ ์ฐ์์ฑ์ ๋์์ ํ๋ณดํ์ต๋๋ค.
- Orchestrator: Intent ๋ถ๋ฅ๋ถํฐ Model ์ ํ, Tool Call Loop, Auto-Fallback๊น์ง โ ์ฌ์ฉ์๋ ๊ทธ๋ฅ ์ง๋ฌธ๋ง ํ๋ฉด ๋ฉ๋๋ค.
๐ ๊ด๋ จ ๋งํฌ
OpenPersona GitHub
ํ๋ก์ ํธ ์์ค ์ฝ๋ ๋ฐ ๋ฌธ์
v1 ๊ตฌ์ถ๊ธฐ ํฌ์คํธ
Electron + Multi-LLM ์ํคํ ์ฒ ์ค๊ณ
RRF ๋ ผ๋ฌธ (Cormack 2009)
Reciprocal Rank Fusion ์๋ฌธ
๐ท๏ธ ํ๊ทธ
#rag #hybrid_search #reranking #orchestration #ai_agent #electron #typescript #gemini #vector_search #bm25