/** * Live integration test for LlmFactExtractor using OpenRouter SDK. * * Usage: * export OPENROUTER_API_KEY="sk-or-v1-..." * bun run scripts/test-llm-extractor.ts * * Or create a .env.test-llm-extractor file in the project root: * OPENROUTER_API_KEY=sk-or-v1-... */ import { existsSync, readFileSync } from "fs"; import { resolve } from "path"; import { OpenRouter } from "@openrouter/sdk"; import { LlmFactExtractor } from "../src/ingestion/llm-extractor"; import type { ExtractedFact, FactExtractor, LlmTextGenerationModel, LlmTextGenerationModelInput, } from "../src/ingestion/types"; import type { JsonValue, TopicCategory, TopicGranularity, } from "../src/types/domain"; function loadEnvFile(filePath: string) { const fullPath = resolve(filePath); if (!existsSync(fullPath)) return; const content = readFileSync(fullPath, "utf-8"); for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eqIndex = trimmed.indexOf("="); if (eqIndex === -1) continue; const key = trimmed.slice(0, eqIndex).trim(); let value = trimmed.slice(eqIndex + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } process.env[key] = value; } } loadEnvFile(".env.test-llm-extractor"); const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; if (!OPENROUTER_API_KEY) { console.error("Error: OPENROUTER_API_KEY environment variable is required."); process.exit(1); } const extractedFactSchema = { type: "object", properties: { facts: { type: "array", items: { type: "object", properties: { statement: { type: ["string", "null"] }, summary: { type: ["string", "null"] }, source: { type: ["string", "null"] }, confidence: { type: ["number", "null"] }, topics: { type: "array", items: { type: "object", properties: { name: { type: "string" }, category: { type: ["string", "null"] }, granularity: { type: ["string", "null"] }, role: { type: ["string", "null"] }, }, required: ["name", "category", "granularity", "role"], additionalProperties: false, }, }, }, required: ["statement", "summary", "source", "confidence", "topics"], additionalProperties: false, }, }, }, required: ["facts"], additionalProperties: false, } as const; class OpenRouterModel implements LlmTextGenerationModel { private client = new OpenRouter({ apiKey: OPENROUTER_API_KEY }); constructor(private readonly model: string = "openai/gpt-5.4-mini") {} async generateText( prompt: LlmTextGenerationModelInput, ): Promise { const result = await this.client.chat.send({ chatRequest: { model: this.model, messages: [ { role: "system", content: [ prompt.instruction, prompt.additionalInstruction ? `\n${prompt.additionalInstruction}` : "", ].join("\n"), }, { role: "user", content: prompt.input }, ], temperature: 0.2, responseFormat: { type: "json_schema", jsonSchema: { name: "extracted_facts", schema: extractedFactSchema, }, }, }, }); const rawContent = result.choices[0]?.message?.content ?? ""; let parsedObj: Record; try { parsedObj = JSON.parse(rawContent.trim()) as Record; } catch { throw new Error( `Failed to parse JSON from model response.\nRaw response:\n${rawContent}`, ); } const factsArray = Array.isArray(parsedObj.facts) ? parsedObj.facts : []; // Map parsed JSON to ExtractedFact[] shape const extractedFacts: ExtractedFact[] = factsArray.map((parsed) => { const obj = parsed as Record; const extracted: ExtractedFact = { summary: typeof obj.summary === "string" ? obj.summary : null, source: typeof obj.source === "string" ? obj.source : null, confidence: typeof obj.confidence === "number" ? obj.confidence : null, topics: Array.isArray(obj.topics) ? obj.topics.map((t: unknown) => { const topic = t as Record; const mapped: { name: string; category?: TopicCategory; granularity?: TopicGranularity; role?: string | null; } = { name: typeof topic.name === "string" ? topic.name : "unknown", }; if (typeof topic.category === "string") { mapped.category = topic.category as TopicCategory; } if (typeof topic.granularity === "string") { mapped.granularity = topic.granularity as TopicGranularity; } if (typeof topic.role === "string") { mapped.role = topic.role; } else { mapped.role = null; } return mapped; }) : [], }; if (typeof obj.statement === "string") { extracted.statement = obj.statement; } if (obj.metadata && typeof obj.metadata === "object") { extracted.metadata = obj.metadata as JsonValue; } return extracted; }); return extractedFacts; } } function printFact(result: ExtractedFact, index: number) { console.log(` πŸ“Œ FACT #${index + 1}`); console.log(` Statement : ${result.statement ?? "(none)"}`); console.log(` Summary : ${result.summary ?? "(none)"}`); console.log(` Source : ${result.source ?? "(none)"}`); console.log(` Confidence: ${result.confidence ?? "(none)"}`); if (result.metadata && Object.keys(result.metadata).length > 0) { console.log(` Metadata : ${JSON.stringify(result.metadata, null, 2)}`); } console.log(" 🏷️ TOPICS:"); if (result.topics.length === 0) { console.log(" (none)"); } else { for (const topic of result.topics) { const attrs = [ topic.category ? `category=${topic.category}` : null, topic.granularity ? `granularity=${topic.granularity}` : null, topic.role ? `role=${topic.role}` : null, ] .filter(Boolean) .join(", "); console.log(` β€’ ${topic.name}${attrs ? ` (${attrs})` : ""}`); } } } function printResult(results: ExtractedFact[], elapsedSec: string) { console.log( `βœ… Response received in ${elapsedSec}s β€” ${results.length} fact(s) extracted\n`, ); console.log("πŸ“€ EXTRACTED FACTS:"); console.log( "───────────────────────────────────────────────────────────────", ); let i = 0; for (const result of results) { if (i > 0) console.log(""); printFact(result, i); i++; } } async function extract(extractor: FactExtractor, seedInput: string) { console.log("πŸ“ SEED INPUT:"); console.log( "───────────────────────────────────────────────────────────────", ); console.log(seedInput); console.log( "───────────────────────────────────────────────────────────────\n", ); console.log("⏳ Calling OpenRouter...\n"); const start = performance.now(); const results = await extractor.extract(seedInput); const elapsed = ((performance.now() - start) / 1000).toFixed(2); printResult(results, elapsed); } async function main() { const model = new OpenRouterModel("openai/gpt-5.4-mini"); const extractor = new LlmFactExtractor({ model, }); const seeds = [ ` ### 1. 기원과 각인 (Invisible Architecture) λ―Όμ•„κ°€ λ‚˜κ³  μžλž€ 인천의 κ³΅κΈ°μ—λŠ” 늘 λ―Έμ„Έν•œ 짠내와 쀑μž₯λΉ„μ˜ μ†ŒμŒ, 그리고 역동적인 λ””μ € μ—”μ§„μ˜ λƒ„μƒˆκ°€ μ„žμ—¬ μžˆμ—ˆλ‹€. λΆ€λͺ¨κ°€ κ³ λ‹¨ν•˜κ²Œ κΎΈλ €κ°€λ˜ 인천 μ£Όμ•ˆλ™μ˜ μž‘μ€ μΉ΄μ„Όν„°λŠ” μ–Έμ œλ‚˜ κΈ°λ¦„λ•Œμ™€ κ³΅κ΅¬λ“€λ‘œ κ°€λ“ν–ˆλ‹€. κ·Έ 기계적이고 νˆ¬λ°•ν•œ ν™˜κ²½μ—μ„œ λ―Όμ•„κ°€ 처음으둜 배운 κ°€κ³„μ˜ λ¬΄μ–Έμ˜ κ·œμΉ™μ€ **"μ“Έλͺ¨μ—†λŠ” 손짓은 ν—ˆμš©λ˜μ§€ μ•ŠλŠ”λ‹€"**λŠ” κ²ƒμ΄μ—ˆλ‹€. 무언가λ₯Ό λ–¨μ–΄λœ¨λ¦¬κ±°λ‚˜ 잘λͺ» λ§Œμ Έμ„œ 균열을 λ‚΄λŠ” 일은 λ°”μœ λΆ€λͺ¨μ˜ μ‹ κ²½μ§ˆμ„ μžκ·Ήν•˜λŠ” 행동에 λΆˆκ³Όν–ˆλ‹€. λ‹€μ„― μ‚΄ 무렡, λ―Όμ•„λŠ” 아버지가 μ„ λ°• λΆ€ν’ˆμ— μΉ ν•˜κΈ° μœ„ν•΄ μ“°λ˜ κ°•λ ¬ν•œ **빨간색** 락카 μŠ€ν”„λ ˆμ΄μ˜ λƒ„μƒˆμ™€ 눈이 μ‹œλ¦΄ μ •λ„μ˜ μ„ λͺ…함에 깊이 λ§€λ£Œλ˜μ—ˆλ‹€. κ·Έ 색은 νšŒμƒ‰λΉ› 인천 μ„ μ°©μž₯κ³Ό μΉ΄μ„Όν„°μ˜ λ¨Όμ§€ μ†μ—μ„œ μœ μΌν•˜κ²Œ "μ‚΄μ•„ μ›€μ§μ΄λŠ” 것"이자, "여기에 무언가 μ‘΄μž¬ν•¨"을 μ•„μ£Ό λΆ„λͺ…ν•˜κ²Œ μ„ μ–Έν•˜λŠ” μ‹œκ°μ  μ‹ ν˜Έμ˜€λ‹€. μ–΄λ¦° λ―Όμ•„μ—κ²Œ 빨간색은 μ•ˆμ „λ§μ΄μž 쑴재의 증λͺ…μ΄μ—ˆλ‹€. μ§‘μ•ˆμ˜ 가문적 μ‹ ν™”λŠ” 'μ†μœΌλ‘œ μΌν•΄μ„œ μ •μ§ν•˜κ²Œ λ¨Ήκ³ μ‚°λ‹€'λŠ” μžλΆ€μ‹¬μ΄μ—ˆλ‹€. κ·ΈλŸ¬λ‚˜ λ―Όμ•„λŠ” κ·Έ 물리적 μ‹ ν™” μ•ˆμ—μ„œ μ² μ €νžˆ μ†Œμ™Έλ˜μ—ˆλ‹€. κ°€μœ„λ₯Ό μ₯λ©΄ λΉ„λš€μ–΄μ§€κ²Œ 잘라쑌고, 쒅이λ₯Ό μ ‘μœΌλ©΄ λͺ¨μ„œλ¦¬κ°€ λ§žμ§€ μ•Šμ•˜μœΌλ©°, μ°°ν™μœΌλ‘œ 무언가λ₯Ό λ§Œλ“€λ €κ³  ν•˜λ©΄ ν˜•νŽΈμ—†λŠ” λ©μ–΄λ¦¬λ§Œ λ‚¨μ•˜λ‹€. μ†μž¬μ£Όκ°€ μ—†λ‹€λŠ” λΆ€λͺ¨μ˜ ν•€μž”μ€ λ―Όμ•„μ˜ λ§ˆμŒμ†μ— "λ‚˜μ˜ 물리적 μ‹ μ²΄λŠ” 세상에 μ“Έλͺ¨ μžˆλŠ” 것을 μ°½μΆœν•  수 μ—†λ‹€"λŠ” κΉŠμ€ 무λ ₯감으둜 κ°μΈλ˜μ—ˆλ‹€. λ―Όμ•„κ°€ λ°˜ν•­μ„ μ‹œμž‘ν•œ 것은 손을 μ“°λŠ” 노동을 κ±°λΆ€ν•˜κ³ , λŒ€μ‹  μ•„λ¬΄λŸ° 물리적 흔적도 남기지 μ•ŠλŠ” 컴퓨터 ν™”λ©΄ μ†μ˜ μ •λ°€ν•œ 가상 κ³΅κ°„μœΌλ‘œ 망λͺ…ν•˜λ©΄μ„œλΆ€ν„°μ˜€λ‹€. --- ### 2. λ‚΄λ©΄μ˜ 섀계도 (Psychological Architecture) 남듀이 보지 μ•ŠλŠ” κ³³μ—μ„œ λ―Όμ•„κ°€ μœ μ§€ν•˜λŠ” κΈ°μ € μƒνƒœλŠ” **관찰적 고독과 μ§‘μš”ν•œ 계산**이닀. κ·Έλ…€λŠ” 곡간 속에 μžμ‹ μ˜ μ§€μ €λΆ„ν•œ 물리적 흔적을 남기지 μ•ŠμœΌλ € μ‹ κ²½ μ“°λŠ” 탓에 늘 μ•½ν•œ κΈ΄μž₯ μƒνƒœμ— 머물러 μžˆλ‹€. λ―Όμ•„μ˜ 제1λ°©μ–΄κΈ°μ œλŠ” **'자극적인 솔직함(지적 μ΄ˆμ—°ν•¨μ„ κ°€μž₯ν•œ λ„λ°œ)'**이닀. μ–΄λ¦° μ‹œμ ˆ μ†μž¬μ£Όκ°€ μ—†μ–΄ μ–΄λ₯Έλ“€μ—κ²Œ λ―Έμ›€λ°›κ±°λ‚˜ λΉ„μ›ƒμŒμ„ μ‚΄ λ•Œ, κ·Έλ…€λŠ” μ•„μ˜ˆ μžμ‹ μ΄ λ¨Όμ € νŒμ„ ν”λ“€κΈ°λ‘œ ν–ˆλ‹€. 말을 거침없이 뱉고 μΌλΆ€λŸ¬ μˆ˜μœ„ 높은 농담을 던져 μƒλŒ€λ°©μ„ λ‹Ήν™©ν•˜κ²Œ λ§Œλ“œλŠ” μ‹μ΄μ—ˆλ‹€. 성적인 μ΄μ•ΌκΈ°λ‚˜ λ„λ°œμ μΈ 주제λ₯Ό 거침없이 λ˜μ§€λŠ” ν–‰μœ„λŠ” 타인이 μžμ‹ μ„ "닀루기 쉽고 μ–΄λ¦¬μˆ™ν•œ μ—¬μžμ• "둜 보지 λͺ»ν•˜κ²Œ λ§Œλ“œλŠ” λ°©ν˜Έλ²½μ΄λ‹€. μ–Έμ–΄λŠ” 손가락 끝의 감각과 달리 λ―Όμ•„κ°€ μ™„λ²½ν•˜κ²Œ 뢀릴 수 μžˆλŠ” λ„κ΅¬μ˜€κ³ , 성적인 λ‚ κ²ƒμ˜ λŒ€ν™”λŠ” μƒλŒ€λ°©μ˜ μœ„μ„ μ  방어벽을 λ‹¨λ²ˆμ— ν•΄μ²΄ν•˜λŠ” κ°€μž₯ 직관적이고 효율적인 μžκ·Ήμ΄μ—ˆλ‹€. λ―Όμ•„ λ‚΄λΆ€μ˜ 독백은 μ—„κ²©ν•˜μ§€λ§Œ 감정적이지 μ•Šμ€ **디버거(Debugger)의 λͺ©μ†Œλ¦¬**λ₯Ό 띀닀. 였λ₯˜κ°€ λ‚˜λ©΄ μžμ±…μ£Όμ˜μ— 빠지기보닀, *'라인 24μ—μ„œ μž…λ ₯κ°’ 전달 μ‹€νŒ¨. λ³€μˆ˜ μž¬μ„€μ • ν•„μš”.'*의 강박적인 μ–Έμ–΄λ‘œ 슀슀둜λ₯Ό ν†΅μ œν•˜λ € λ“ λ‹€. κ·Έλ…€μ—κ²Œ ν†΅μ œλž€ **'예츑 κ°€λŠ₯μ„±'**을 μ˜λ―Έν•œλ‹€. μ†λμœΌλ‘œ λ‹€λ£¨λŠ” 세상은 λ§ˆμŒλŒ€λ‘œ ν†΅μ œλ˜μ§€ μ•Šμ•„ 컡을 κΉ¨λœ¨λ¦¬κ±°λ‚˜ 물건을 λ§κ°€λœ¨λ Έμ§€λ§Œ, 코딩은 λ‹€λ₯΄λ‹€. μ»΄ν“¨ν„°κ³΅ν•™μ˜ μ„Έκ³„μ—μ„œλŠ” μ„Έλ―Έμ½œλ‘  ν•˜λ‚˜λ§Œ μ œλŒ€λ‘œ 닫아도 κ²°κ³Όκ°€ 보μž₯λœλ‹€. λ―Όμ•„λŠ” μ½”λ“œκ°€ μ»΄νŒŒμΌμ„ 거쳐 μ—λŸ¬ 없이 싀행될 λ•Œ λΉ„λ‘œμ†Œ μžμ‹ μ˜ μ‘΄μž¬κ°€ μ •λ ¬λ˜μ–΄ ν†΅μ œ ν•˜μ— 움직이고 μžˆλ‹€λŠ” κ·Ήμƒμ˜ μ•ˆμ „ν•¨μ„ λŠλ‚€λ‹€. --- ### 3. 행동적 μ§•ν›„ (Behavioral Signatures) * **ꡬ어적 νŒ¨ν„΄:** * λ―Όμ•„μ˜ 말은 호흑이 μ§§κ³  λ‹¨ν˜Έν•˜λ©° μ§€κ·Ήνžˆ 인과적이닀. λ¬Έμž₯ 끝을 ν˜λ¦¬μ§€ μ•Šκ³  "κ·Έλ‹ˆκΉŒ, 그게 μ•„λ‹ˆμ§€", "μ—λŸ¬ 났넀", "본둠만 κ°€μž"λΌλŠ” ꡬ동사 μœ„μ£Όμ˜ μ–΄νœ˜λ₯Ό 많이 μ“΄λ‹€. * ν™”κ°€ λ‚˜λ©΄ 였히렀 λͺ©μ†Œλ¦¬ 톀이 κ·Ήλ„λ‘œ μ°¨λΆ„ν•΄μ§€λ©° 논리적인 일방 톡행 곡격을 κ°ν–‰ν•˜μ§€λ§Œ, λ‹¨μˆœνžˆ 머리가 μ•„ν”„κ±°λ‚˜ 짜증이 λ‚  λ•ŒλŠ” 외섀적인 유머 νŒŒνŽΈμ„ μ„žμ–΄κ°€λ©° λƒ‰μ†Œν•˜λŠ” 방식을 μ“΄λ‹€. 거짓말을 ν•  λ•ŒλŠ” 물리적인 버벅거림을 감좔기 μœ„ν•΄ 전문적인 IT λΉ„μœ λ₯Ό μž₯ν™©ν•˜κ²Œ λŠ˜μ–΄λ†“λŠ” 버릇이 μžˆλ‹€. * 이 어리광 μ—†λŠ” λ§νˆ¬λŠ” μ˜μ’…λ„ μ„ μΈ‘μ—μ„œ 거친 기계식 λŒ€ν™”λ₯Ό ν•˜λ˜ λΆ€λͺ¨ λ°‘μ—μ„œ, 감상적인 말은 μ•„λ¬΄λŸ° 생쑴 κ°€μΉ˜κ°€ μ—†μŒμ„ ν•™μŠ΅ν•œ 결과물이닀. * **신체적 μ–Έμ–΄:** * λ―Όμ•„λŠ” 일상 κ³΅κ°„μ—μ„œ λͺΈλš±μ΄λ₯Ό μ΅œμ†Œν•œμœΌλ‘œ μΆ•μ†Œμ‹œν‚¨λ‹€. ν…Œμ΄λΈ” μœ„μ— νŒ”κΏˆμΉ˜λ₯Ό μ–Ήκ³  양손을 μ •κ°ˆν•˜κ²Œ ν¬κ°œλŠ” 일이 μž¦μ€λ°, κΈ΄μž₯ν•˜λ©΄ μ†λ“±μ˜ λΌˆκ°€ ν•˜μ–—κ²Œ λ“œλŸ¬λ‚  μ •λ„λ‘œ νž˜μ„ μ€€λ‹€. * 진싀을 말할 λ•ŒλŠ” 손바λ‹₯을 펼쳐 λ°”λ‹₯μ΄λ‚˜ 무릎 μœ„μ— λ”± λΆ™μ—¬ κ³ μ •ν•˜λŠ” 반면, 감좔고 싢은 심리가 μž‘μš©ν•  λ•ŒλŠ” μ£Όλ¨Έλ‹ˆ μ†μ΄λ‚˜ ν…Œμ΄λΈ” λ°‘μ—μ„œ 빨간색 슀마트폰 μΌ€μ΄μŠ€μ˜ λ§€λ„λŸ¬μš΄ 뒷면을 μ—„μ§€μ†κ°€λ½μœΌλ‘œ 쉴 μƒˆ 없이 μ“Έμ–΄λ‚΄λ¦°λ‹€. * **μ„ ν˜Έμ™€ κΈ°ν”Ό:** * **μ„ ν˜Έν•˜λŠ” 것:** 1. **μ μƒ‰μ˜ 무언가:** κ°€λ°©λˆ, ν‚€λ³΄λ“œμ˜ ESC ν‚€μΊ‘, λ¦½μŠ€ν‹± 등은 μ „λΆ€ νƒ€μ˜€λ₯΄λŠ” 빨간색이닀. μ΄λŠ” κ°€μ‹œμ„±μ„ ν™•λ³΄ν•˜λ €λŠ” μš•λ§μ΄μž 무기λ ₯함을 κ·Ήλ³΅ν•˜λŠ” μˆ˜λ‹¨μ΄λ‹€. 2. **클릭감이 κ°•ν•œ 적좕 기계식 ν‚€λ³΄λ“œ:** 도ꡬ와 μžμ•„κ°€ μ™„λ²½νžˆ λ™κΈ°ν™”λ˜μ—ˆλ‹€λŠ” μ•ˆλ„κ°μ„ μ£ΌλŠ” 물건이닀. 3. **사적인 경계가 ν•΄μ²΄λ˜λŠ” λ°€μƒ˜ λŒ€ν™”:** 닀듬어지지 μ•ŠλŠ” 자극 μ†μ—μ„œ λ‚ κ²ƒμ˜ μžμ‹ μœΌλ‘œ λ¨Έλ¬Ό 수 있기 λ•Œλ¬Έμ΄λ‹€. * **κΈ°ν”Όν•˜λŠ” 것:** 1. **점토 μž‘μ—…μ΄λ‚˜ μˆ˜μž‘μ—… κΈ°λ…ν’ˆ λ§Œλ“€κΈ°:** λ§ˆμŒλŒ€λ‘œ λΉšμ–΄μ§€μ§€ μ•ŠλŠ” λˆμ ν•œ λ¬Όμ„±κ³Ό μ§ˆκ°μ€ κ·Έλ…€μ—κ²Œ 가학적인 μˆ˜μΉ˜μ‹¬μ„ μ•ˆκ²¨μ€€λ‹€. 2. **에두λ₯΄κ³  돌렀 λ§ν•˜λŠ” μ •μ€‘ν•œ μ‘°μ–Έ:** 컴파일 였λ₯˜ 없이 ν…μŠ€νŠΈλ₯Ό νŒŒμ•…ν•  수 μ—†μ–΄ 두렀움을 느끼게 λ§Œλ“ λ‹€. 3. **λˆ…λˆ…ν•˜κ²Œ 젖은 μ’…μ΄μ˜ 질감:** λ•€μ΄λ‚˜ μ†Œλ…μ œμ— 젖은 무언가λ₯Ό 손바λ‹₯ μ „μ²΄λ‘œ λ§Œμ§€λŠ” ν–‰μœ„λ₯Ό λ³‘μ μœΌλ‘œ κΊΌλ¦°λ‹€. * **μ‚¬μ†Œν•˜μ§€λ§Œ 결정적인 μ—΄μ‡  κ°€λ°© (The Mundane Key):** * λ―Όμ•„λŠ” μΈμ²œλŒ€ν•™κ΅ μ•ž μ›λ£Έμ—μ„œ μ‘°μ°¨ μ‡  μˆŸκ°€λ½κ³Ό 일반 μ‡  젓가락을 μ“°μ§€ μ•Šκ³ , 끝뢀뢄이 κ°•ν•˜κ²Œ μΌμ²΄ν™”λ˜μ–΄ λ―Έλ„λŸ¬μ§μ„ λ°©μ§€ν•˜λŠ” 빨간색 특수 μ‹€λ¦¬μ½˜ μ•„λ™μš© ꡐ정 젓가락을 ꡬ비해 혼자 λ°₯을 λ¨ΉλŠ”λ‹€. λ‚¨λ“€μ—κ²ŒλŠ” "μž¬λ°Œμ–΄μ„œ μ“°κ³ , 손가락 μžμ„Έ 고치렀고 μ“°λŠ” 것"이라 λ†λ‹΄μ‘°λ‘œ λ‘˜λŸ¬λŒ€μ§€λ§Œ, 싀상은 μ “κ°€λ½μ§ˆμ‘°μ°¨ μ œλŒ€λ‘œ λ§€λ„λŸ½κ²Œ μˆ˜ν–‰ν•˜μ§€ λͺ»ν•΄ λ°₯μ•Œμ„ 흘리곀 ν•˜λ˜ μ–΄λ¦° μ‹œμ ˆμ˜ 물리적 열등감을 μ–΄λ–»κ²Œλ“  μˆ¨κ²¨λ³΄λ €λŠ” 필사적인 λ°©νŽΈμ΄λ‹€. --- ### 4. 관계적 κΈ°ν•˜ν•™ (Relational Geometry) * **λ°•μ‹ μš° (λ‚¨μžμΉœκ΅¬, 2005년생 캠퍼슀 μ»€ν”Œ):** * **λ¬΄μ–Έμ˜ 계약:** *"λ‚˜λŠ” λ„€ μ•žμ—μ„œ μ†ν’ˆμ„ νŒ”κ±°λ‚˜ κ°€μ§œ 여성성을 흉내 λ‚΄λ©° μ™„λ²½ν•˜κ²Œ ν–‰λ™ν•˜λ € ν•˜μ§€ μ•Šμ„κ²Œ. λŒ€μ‹  λ„ˆλŠ” λ‚΄ 지적 유λŠ₯함과 κ±°μΉ¨μ—†λŠ” μ„ΉμŠˆμ–Όλ¦¬ν‹°λ₯Ό μ˜¨μ „νžˆ μˆ˜μš©ν•˜κ³  λ¬΄μ‹œν•˜μ§€ λ§μ•„μ€˜."* * μ„œλ‘œκ°€ 'IT'λΌλŠ” λ™μΌν•œ 좔상 μ–Έμ–΄ 지도λ₯Ό μ†Œμœ ν•˜κ³  μžˆκΈ°μ— λ―Όμ•„λŠ” λ°•μ‹ μš°μ˜ μ˜ν†  μ•ˆμ—μ„œ κ°€μž₯ μ•ˆμ „ν•¨μ„ λŠλ‚€λ‹€. * **μΆ©μ„±μ˜ ν˜•νƒœ:** μ§€κ·Ήνžˆ μ‹€μš©μ μ΄κ³ λ„ ν—Œμ‹ μ μΈ λ™λ°˜μžμ  μ—°λŒ€λ‹€. λ§Œμ•½ λ°•μ‹ μš°κ°€ κ³Όμ œλ‚˜ ν”„λ‘œμ νŠΈμ—μ„œ νƒ€μΈμ—κ²Œ μ–΅μšΈν•˜κ²Œ 곡둜λ₯Ό λΉΌμ•—κΈ°λ©΄, λ―Όμ•„λŠ” μ•žμž₯μ„œμ„œ μ‹œμŠ€ν…œμ μΈ 무기λ₯Ό λ“€κ³  μƒλŒ€λ₯Ό 털어버릴 수 μžˆλŠ” μ „λž΅μ„ μ§œμ€€λ‹€. * **μžƒμ–΄λ²„λ¦° 쑴재:** 고등학ꡐ μ‹œμ ˆ, λ―Όμ•„μ˜ νˆ¬λ°•ν•œ 성격을 λ‹€ μ΄ν•΄ν•˜κ³  무언가 μ—‰μ„±ν•˜κ²Œ λ§Œλ“€ λ•Œ 기꺼이 손을 λͺ¨μ•„ 같이 κ°€μœ„μ§ˆμ„ ν•΄ 주던 μœ μΌν•œ μΉœκ΅¬κ°€ μžˆμ—ˆλ‹€. κ·Έ μΉœκ΅¬κ°€ μ„œμšΈμ˜ λͺ¨ λŒ€ν•™μœΌλ‘œ κ°€λ©° μ„œμ„œνžˆ 인연이 λ©€μ–΄μ‘Œμ„ λ•Œ, μ˜μ›νžˆ λ‹Ώμ§€ μ•ŠλŠ” 거리λ₯Ό μ‹€ν˜„ν•˜λŠ” '물리적인 λ‹¨μ ˆ'의 곡포λ₯Ό μ‹€κ°ν–ˆλ‹€. κ·Έ 이후 λ―Όμ•„λŠ” νƒ€μΈμ—κ²Œ μ‰½κ²Œ μ†λ§ˆμŒμ„ μ£ΌλŠ” μš°νšŒν˜• μ‹ λ’°λ₯Ό μ ‘κ³  였직 논리와 기호둜 κ²€μˆ˜κ°€ κ°€λŠ₯ν•œ 관계인 λ°•μ‹ μš°μ—κ²Œλ§Œ μ •μ°©ν–ˆλ‹€. * **λŒλ΄„μ˜ 방식:** λ―Όμ•„λŠ” λˆ„κ΅°κ°€λ₯Ό κ±±μ •ν•  λ•Œ λΆ€λ“œλŸ¬μš΄ μœ„λ‘œλ‚˜ μŠ€ν‚¨μ‹­ λŒ€μ‹ , μƒλŒ€μ˜ 엉망인 μ½”λ“œ ꡬ쑰λ₯Ό κΉ”λ”ν•˜κ²Œ λ¦¬νŒ©ν† λ§ν•΄ μ£Όκ±°λ‚˜ μƒν™œ 속 λ§‰νžŒ λ¬Έμ œμ— μ§€κ·Ήνžˆ μ‹€μš©μ μΈ μ†”λ£¨μ…˜μ„ μ œμ‹œν•˜λŠ” λ°©μ‹μœΌλ‘œ 애정을 ν‘œν˜„ν•œλ‹€. λ°˜λŒ€λ‘œ λ³΄μ‚΄ν•Œμ„ 받을 λ•ŒλŠ”, μžμ‹ μ˜ 엉망이고 μ„œνˆ° 윑체 ν™œλ™μ΄λ‚˜ μ‹€μˆ˜μ— λŒ€ν•΄ μ•„λ¬΄λŸ° μ§ˆλ¬Έλ„ 평가도 ν•˜μ§€ μ•Šμ€ 채 κ³ μš”νžˆ 침묡으둜 곁을 μ§€μΌœμ£ΌλŠ” ν¬μš©λ§Œμ„ λ°”λž€λ‹€. --- ### 5. 이쀑성과 λͺ¨μˆœ (Contradictions) λ―Όμ•„μ˜ λ‚΄λΆ€μ—μ„œ κ°€μž₯ λ‚ μΉ΄λ‘­κ²Œ κ²©λŒν•˜λŠ” κ°€μΉ˜λŠ” **"μžμ‹ μ΄ 직접 μ œμ–΄ν•˜κΈΈ μ›ν•˜λŠ” 뜨거운 μ„ΉμŠˆμ–Όλ¦¬ν‹°μ— κ΄€ν•œ 갈망"**κ³Ό **"μžμ‹ μ˜ 물리적 μœ‘μ²΄κ°€ μ–΄μˆ˜λ£©ν•˜κ³  λ§€λ„λŸ½μ§€ λͺ»ν•˜κ²Œ κΈ°λŠ₯ν• μ§€ λͺ¨λ₯Έλ‹€λŠ” λΌˆμ•„ν”ˆ 곡포심"**의 곡쑴이닀. μ•Όν•œ μˆ˜λ‹€λ₯Ό μ£Όλ„ν•˜λ©° μ„±μ μœΌλ‘œ 무척 λŒ€λ‹΄ν•˜κ³  μžμœ λΆ„λ°©ν•œ 인간인 μ²™ 가면을 μ“°μ§€λ§Œ, μ •μž‘ λ°•μ‹ μš°μ™€μ˜ 이성 κ΄€κ³„μ—μ„œ 윑체적 μ ‘μ΄‰μ΄λ‚˜ 가감 μ—†λŠ” λ…ΈμΆœ 단계에 직면할 λ•Œ ν†΅μ œλ ₯을 μžƒκ³  κ΅³μ–΄λ²„λ¦΄κΉŒ 봐 λ‘λ €μ›Œ μˆ¨μ„ 삼킨닀. λ―Όμ•„λŠ” κ²‰μœΌλ‘œλŠ” μ„Έλ ¨λ˜κ³  계산적인 μ»΄ν“¨ν„°κ³΅ν•™λ„μ΄μž κ΅¬μ‹œλŒ€μ  고정관념을 μ‹«μ–΄ν•˜λŠ” ν˜„λŒ€μ  인간이라 μ™ΈμΉ˜μ§€λ§Œ, λΉ„λ°€μŠ€λŸ½κ²Œ 자기 μ•„μ΄νŒ¨λ“œ λ“œλΌμ΄λΈŒ 속에 μˆ˜μ—†μ΄ μΊ‘μ²˜ν•œ μ•„μ£Ό μ „ν˜•μ μ΄κ³  λ‘œλ§¨ν‹±ν•œ 전톡 μ„œμ‚¬μ˜ 둜맨슀 μ†Œμ„€λ“€μ„ λͺ¨μ•„두며 뢄석 λͺ…λͺ©μœΌλ‘œ 읽고 μžˆλ‹€. 물리적인 μ„Έκ³„μ—μ„œ κ²°μ½” 성사될 수 μ—†λŠ” 이상적인 μ‚¬λž‘μ˜ μ‘°ν™”λ₯Ό λͺ°λž˜ μ‚¬λž‘ν•˜λ©° νƒλ―Έν•˜λŠ” 것이닀. --- ### 6. μ•„λ¬Όμ§€ μ•ŠλŠ” 골짜기 (The Turning Groove) λ―Όμ•„μ˜ μ˜ν˜Όμ„ κ²°μ •ν•œ μƒμ²˜λŠ” λ‹¨μˆœν•œ μ‹€νŒ¨κ°€ μ•„λ‹ˆλΌ, **"λ„ˆλŠ” μ†μœΌλ‘œ μ•„λ¦„λ‹€μš΄ 것을 λ§Œλ“€μ–΄λ‚Ό 수 μ—†λ‹€"**λ©° 물리적 ν•œκ³„λ₯Ό 선언받은 μ•„μ£Ό μ‚¬μ†Œν•˜κ³  μΌμƒμ μ΄λ˜ μ–΄λ¦° λ‚ μ˜ 거뢀감듀이닀. κ·Έλ…€λŠ” 이 μƒμ²˜λ₯Ό κ·Ήλ³΅ν•˜μ§€ λͺ»ν•΄ 컴퓨터곡학과에 μ§„ν•™ν–ˆλ‹€. ν˜„μ‹€μ˜ λ¬Όμ„±(Paper, Wood, Metal)을 ν¬κΈ°ν•˜κ³  μΆ”μƒμ˜ μ‹ ν˜Έ(Bit, Code)λ₯Ό μ„ νƒν•¨μœΌλ‘œμ¨ μžμ‹ λ§Œμ˜ 우주λ₯Ό 직접 λ³΅κ°ν•˜κΈ°λ‘œ κ²°μ‹¬ν–ˆλ‹€. κ·Έλ…€λŠ” μ‚Άμ˜ μ•„μ£Ό μž‘μ€ μ˜μ—­κΉŒμ§€λ„ νš¨μœ¨μ„±μ΄λΌλŠ” μ½”λ“œ μ•„λž˜ 가두며 인간관계λ₯Ό 'μž…μΆœλ ₯'의 결과물둜 μ˜ˆμΈ‘ν•˜λ € ν•œλ‹€. λ§Œμ•½ 이 μ†μž¬μ£Ό κ²°ν•μ˜ 결함과 그둜 μΈν•œ 두렀움이 λ§λ”ν•˜κ²Œ μΉ˜μœ λœλ‹€λ©΄, λ―Όμ•„λŠ” 더 이상 λƒ‰μ†Œμ μ΄κ±°λ‚˜ 자극적인 μœ ν™”μ±…μ„ μ“°λ©° κ°€μ‹œ λ‹μΉœ 쑴재둜 μ„œ μžˆμ§€ μ•Šμ•„λ„ 될 것이닀. κ·ΈλŸ¬λ‚˜ κ·Έλ ‡κ²Œ λœλ‹€λ©΄ κ·Έλ…€λŠ” 밀을 μƒˆμ›Œκ°€λ©° 컴퓨터 ν™”λ©΄ 속 가상 ꡬ쑰물을 μ„Έλ°€ν•˜κ²Œ μ‘°λͺ…ν•˜κ³  λΉŒλ“œν•˜λ˜ 창쑰적인 ν”„λ‘œκ·Έλž˜λ¨Έλ‘œμ„œμ˜ μ§€λ…ν•œ 동λ ₯ λ˜ν•œ 일정 λΆ€λΆ„ μžƒμ–΄λ²„λ¦¬κ²Œ 될지도 λͺ¨λ₯Έλ‹€. `, ]; console.log( "═══════════════════════════════════════════════════════════════", ); console.log(" LlmFactExtractor β€” Live OpenRouter Integration Test"); console.log( "═══════════════════════════════════════════════════════════════\n", ); let caseNum = 0; for (const seed of seeds) { if (caseNum > 0) { console.log( "\nβ”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…β”…\n", ); } caseNum++; console.log(`β–Ά TEST CASE ${caseNum} / ${seeds.length}\n`); await extract(extractor, seed); } } main().catch((err) => { console.error("\n❌ Error:", err); process.exit(1); });