DOCS/workflow/_archive/companyx_grounding_pipeline.json
happybell80 45847403fe docs: workflow Phase 1 정리 — 레거시 이동, n8n 제거, IP 하드코딩 제거
- 레거시 .md 2건(slack_basic_dialogue, service_health_check)을 _archive/로 이동
- n8n .json 파일 18건을 _archive/로 이동
- README.md 전면 재작성: n8n 전제 제거, 현행 아키텍처(APScheduler DB + SKILL.md) 반영
- 구 IP(192.168.219.52)를 localhost로 교체 (infrastructure-ssot-principle 준수)

Refs: DOCS#8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 07:48:39 +09:00

182 lines
8.5 KiB
JSON

{
"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"
}
}