{ "name": "robeing-rag-companyx-grounding-pipeline", "nodes": [ { "parameters": { "content": "## 260319 변경 영향 없음\n\nrb8001에 프롬프트 DB v3 및 neutral constraints 생략이 적용되었으나, 이 워크플로우는 LLM Answer with Context 노드에서 system_instruction을 직접 지정하므로 rb8001 기본 프롬프트가 주입되지 않음.\n\n상세: companyx_grounding_pipeline.md", "height": 200, "width": 400 }, "type": "n8n-nodes-base.stickyNote", "position": [-200, 60], "typeVersion": 1, "id": "rag-note-260319", "name": "260319 Change Note" }, { "parameters": { "httpMethod": "POST", "path": "rag/companyx/grounding", "responseMode": "responseNode", "options": {} }, "id": "cx-rag-001", "name": "Webhook In", "type": "n8n-nodes-base.webhook", "typeVersion": 1, "position": [200, 240], "notes": "Company X 소속 사용자의 내부 문서 질문 수신" }, { "parameters": { "jsCode": "const body = $json.body || {};\nreturn {\n query: body.query || '',\n user_uuid: body.user_id || '',\n team_id: '79441171-3951-4870-beb8-916d07fe8be5',\n limit: body.limit || 5,\n threshold: 0.35\n};" }, "id": "cx-rag-002", "name": "Set Context (Company X)", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [440, 240], "notes": "Company X 팀 ID 고정, 검색 파라미터 설정" }, { "parameters": { "jsCode": "const message = $json.query;\nconst lowered = message.toLowerCase();\nlet questionType = 'fact_check';\nif (['다시 정리', '정리해줘', '문서명만', '근거 문서', '다시 보여'].some(k => lowered.includes(k))) {\n questionType = 'recap';\n} else if (['휴가', '연차', '반차', '규정', '복지', '취업규칙', '인사'].some(k => lowered.includes(k))) {\n questionType = 'fact_check';\n} else if (['몇개', '몇 개', '건수', '몇 건', '얼마나', '수는', '수가', '개수'].some(k => lowered.includes(k))) {\n questionType = 'quantitative';\n} else if (['뭐야', '무엇', '설명', '소개', '알려줘'].some(k => lowered.includes(k))) {\n questionType = 'explanatory';\n}\nreturn { ...$json, question_type: questionType };" }, "id": "cx-rag-002b", "name": "Classify Question Type", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [660, 240], "notes": "질문 유형 분류 (LLM 프롬프트 톤 조절용)" }, { "parameters": { "method": "POST", "url": "=http://192.168.0.106:8508/api/search", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"query\": \"{{ $json.query }}\",\n \"team_id\": \"{{ $json.team_id }}\",\n \"limit\": {{ $json.limit }},\n \"threshold\": {{ $json.threshold }}\n}", "options": {} }, "id": "cx-rag-003", "name": "Vector Search (skill-rag-file)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [900, 240], "notes": "벡터 유사도 검색. 키워드 필터링 없이 유사도 상위 결과를 그대로 반환한다." }, { "parameters": { "jsCode": "const results = $json.body?.results || [];\nif (results.length === 0) {\n return { has_results: false, context: '', evidence_filenames: [], question_type: $('Classify Question Type').item.json.question_type };\n}\n\n// 벡터 유사도 순으로 정렬, 문서 다양성 기준 상위 5개 선택 (키워드 필터링 없음)\nconst sorted = results.sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0));\nconst seenDocs = new Set();\nconst selected = [];\nfor (const r of sorted) {\n const docId = r.document_id || '';\n if (docId && seenDocs.has(docId)) continue;\n if (docId) seenDocs.add(docId);\n selected.push(r);\n if (selected.length >= 5) break;\n}\n\nconst context = selected.map(r => `파일명: ${r.filename}\\n내용: ${r.chunk_text}`).join('\\n\\n---\\n\\n');\nconst filenames = selected.map(r => r.filename);\nconst original = $('Classify Question Type').item.json;\n\nreturn {\n has_results: true,\n message: original.query,\n question_type: original.question_type,\n grounding_context: context,\n evidence_filenames: filenames,\n user_id: original.user_uuid,\n result_count: selected.length\n};" }, "id": "cx-rag-004", "name": "Select Top Results (Vector Score)", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1140, 240], "notes": "벡터 유사도 점수 기준 상위 선택. 룰베이스 절제 원칙(§B.6)에 따라 키워드 필터링을 사용하지 않는다. LLM이 적합도를 재판단한다." }, { "parameters": { "conditions": { "boolean": [ { "value1": "={{ $json.has_results }}", "value2": true } ] } }, "id": "cx-rag-004b", "name": "Has Results?", "type": "n8n-nodes-base.if", "typeVersion": 1, "position": [1380, 240], "notes": "검색 결과 0건이면 실패 경로로 분기" }, { "parameters": { "method": "POST", "url": "=http://192.168.0.106:8001/api/message", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "X-User-Id", "value": "={{ $json.user_id }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"message\": \"{{ $json.message }}\",\n \"context\": {\n \"system_instruction\": \"당신은 Company X 내부 문서 기반 답변 전문가입니다. 제공된 컨텍스트만 근거로 사용하세요. 질문 유형: {{ $json.question_type }}. 컨텍스트만으로 부족하면 failure_reason에 이유를 적고 direct_answer는 빈 문자열로 두세요.\\n\\n컨텍스트:\\n{{ $json.grounding_context }}\"\n },\n \"source\": \"n8n-grounding\"\n}", "options": {} }, "id": "cx-rag-005", "name": "LLM Answer with Context", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1620, 140], "notes": "LLM이 컨텍스트를 보고 답변 가능 여부를 직접 판단한다. Pydantic 검증은 rb8001 코드에서 수행." }, { "parameters": { "respondWith": "json", "responseBody": "={{ JSON.stringify({ success: true, type: 'companyx_rag', message: $json.body?.message || $json.body, rag_used: true, grounding_present: true }) }}", "options": {} }, "id": "cx-rag-006", "name": "Return Grounded Answer", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [1860, 140], "notes": "LLM 답변 + 근거 문서 반환" }, { "parameters": { "respondWith": "json", "responseBody": "={{ JSON.stringify({ success: true, type: 'companyx_rag', message: '현재 확인된 Company X 내부 문서 기준으로는 질문과 맞는 근거를 확인하지 못했습니다.', rag_used: true, grounding_present: false }) }}", "options": {} }, "id": "cx-rag-007", "name": "Return Failure (No Results)", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [1620, 380], "notes": "검색 결과 0건 — 실패 가시성 원칙(§B.5)에 따라 명시적 실패 응답" } ], "connections": { "Webhook In": { "main": [[{ "node": "Set Context (Company X)", "type": "main", "index": 0 }]] }, "Set Context (Company X)": { "main": [[{ "node": "Classify Question Type", "type": "main", "index": 0 }]] }, "Classify Question Type": { "main": [[{ "node": "Vector Search (skill-rag-file)", "type": "main", "index": 0 }]] }, "Vector Search (skill-rag-file)": { "main": [[{ "node": "Select Top Results (Vector Score)", "type": "main", "index": 0 }]] }, "Select Top Results (Vector Score)": { "main": [[{ "node": "Has Results?", "type": "main", "index": 0 }]] }, "Has Results?": { "main": [ [{ "node": "LLM Answer with Context", "type": "main", "index": 0 }], [{ "node": "Return Failure (No Results)", "type": "main", "index": 0 }] ] }, "LLM Answer with Context": { "main": [[{ "node": "Return Grounded Answer", "type": "main", "index": 0 }]] } }, "settings": {}, "pinData": {}, "meta": { "templateCredsSetupCompleted": true, "instanceId": "robeing-rag" } }