docs: Phase 3 키워드 필터링→LLM 위임 반영, n8n JSON 워크플로우 갱신
- 계획: Phase 3 근거 채택 판정을 LLM 위임으로 전환 반영 - 워크플로우 JSON: 키워드 필터링 노드 제거, Vector Score 선택 + LLM 판단 구조 - 워크플로우 MD: 근거 선별 원칙 추가 (룰베이스 절제 원칙 §B.6) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
64ef0deaee
commit
dd786dfed6
@ -276,14 +276,12 @@ tags: [plans, companyx, rag, answer-composition, scenario, troubleshooting]
|
||||
- `그럼 컴퍼니엑스 내부 규정 상 휴가는 얼마나 쓸 수 있어?` -> 사실 확인형 또는 규정 확인형 성격
|
||||
- `오늘전통 프로그램을 Company X가 옐로펀치랑 같이 운영한다는 근거 있어?` -> 사실 확인형
|
||||
|
||||
### Phase 3. 근거 채택 판정 추가 — **구현 완료**
|
||||
### Phase 3. 근거 채택 판정 — **LLM 위임으로 전환**
|
||||
- 검색 결과를 그대로 상위 3개 노출하지 않습니다.
|
||||
- 질문 유형에 맞지 않는 청크는 버립니다.
|
||||
- 필요 최소 판정:
|
||||
- 엔티티 일치 여부
|
||||
- 문서 성격 일치 여부
|
||||
- 답변 가능 근거인지 여부
|
||||
- `휴가` 질문에 `todaytradition` 청크가 잡히는 경우는 `무관한 결과`로 실패 처리해야 합니다.
|
||||
- **키워드 기반 룰 필터링은 룰베이스 절제 원칙(global-principles.md §B.6)에 따라 제거했습니다.**
|
||||
- 벡터 유사도 상위 결과를 문서 다양성 기준으로 선택한 뒤, LLM에 컨텍스트로 전달합니다.
|
||||
- LLM이 컨텍스트를 보고 질문 적합도를 재판단하며, 무관한 청크는 LLM이 `failure_reason`으로 처리합니다.
|
||||
- `휴가` 질문에 `todaytradition` 청크가 잡히는 경우는 LLM이 `질문과 맞는 문서 미확인`으로 답해야 합니다.
|
||||
|
||||
### Phase 4. LLM 기반 답변 생성 + Pydantic 출력 검증 — **구현 완료**
|
||||
- 현재 `_build_direct_answer()` 규칙 문자열 조합을 LLM 호출로 대체합니다.
|
||||
|
||||
@ -12,47 +12,80 @@
|
||||
"name": "Webhook In",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 240]
|
||||
"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', // Company X Team ID\n limit: body.limit || 5\n};"
|
||||
"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]
|
||||
"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.219.52:8508/api/search",
|
||||
"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}",
|
||||
"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": "Search Internal Docs",
|
||||
"name": "Vector Search (skill-rag-file)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [680, 240]
|
||||
"position": [900, 240],
|
||||
"notes": "벡터 유사도 검색. 키워드 필터링 없이 유사도 상위 결과를 그대로 반환한다."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const results = $json.body || [];\nconst context = results.map(r => `[출처: ${r.filename}]\\n${r.text}`).join('\\n---\\n');\nconst original = $('Set Context (Company X)').item.json;\nreturn {\n message: original.query,\n grounding_context: context,\n user_id: original.user_uuid,\n robeing_id: 'rb8001'\n};"
|
||||
"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": "Prepare LLM Prompt",
|
||||
"name": "Select Top Results (Vector Score)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [940, 240]
|
||||
"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.219.52:8001/api/chat",
|
||||
"url": "=http://192.168.0.106:8001/api/message",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
@ -64,36 +97,73 @@
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"message\": \"{{ $json.message }}\",\n \"context\": \"아래의 컴퍼니엑스 내부 문서 내용을 바탕으로 답변하세요:\\n{{ $json.grounding_context }}\",\n \"source\": \"n8n-grounding\"\n}",
|
||||
"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": "Call rb8001 with Context",
|
||||
"name": "LLM Answer with Context",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1200, 240]
|
||||
"position": [1620, 140],
|
||||
"notes": "LLM이 컨텍스트를 보고 답변 가능 여부를 직접 판단한다. Pydantic 검증은 rb8001 코드에서 수행."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json.body }}",
|
||||
"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": [1460, 240]
|
||||
"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": "Search Internal Docs", "type": "main", "index": 0 }]] },
|
||||
"Search Internal Docs": { "main": [[{ "node": "Prepare LLM Prompt", "type": "main", "index": 0 }]] },
|
||||
"Prepare LLM Prompt": { "main": [[{ "node": "Call rb8001 with Context", "type": "main", "index": 0 }]] },
|
||||
"Call rb8001 with Context": { "main": [[{ "node": "Return Grounded Answer", "type": "main", "index": 0 }]] }
|
||||
"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" }
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "robeing-rag"
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,13 +26,19 @@ tags: [workflow, rag, companyx, grounding, answer]
|
||||
|
||||
## 처리 순서
|
||||
1. 질문이 Company X 근거응답 대상인지 판단한다.
|
||||
2. 검색된 문서 중 질문과 관련도가 높은 근거만 선별한다.
|
||||
3. 직접 답을 먼저 만든다.
|
||||
4. 근거 문서명과 요약을 붙인다.
|
||||
5. 근거가 부족하면 추정 답변 대신 실패 응답으로 끝낸다.
|
||||
2. 벡터 유사도 검색으로 상위 결과를 수집한다.
|
||||
3. 검색 결과를 LLM에 컨텍스트로 전달하고, LLM이 질문 적합도를 판단해 답변한다.
|
||||
4. LLM 응답을 Pydantic으로 검증한다.
|
||||
5. 근거가 부족하면 LLM이 `failure_reason`을 명시하고, 추정 답변 대신 실패 응답으로 끝낸다.
|
||||
|
||||
## 근거 선별 원칙
|
||||
- 키워드 기반 룰로 검색 결과를 필터링하지 않는다 (룰베이스 절제 원칙 — global-principles.md §B.6).
|
||||
- 벡터 검색이 반환한 유사도 순서를 신뢰하고, LLM이 컨텍스트를 보고 적합도를 재판단한다.
|
||||
- 근거 선별 책임은 LLM에 있으며, 코드 레벨에서는 유사도 상위 결과를 문서 다양성 기준으로 선택만 한다.
|
||||
|
||||
## 실패 분기
|
||||
- 질문과 맞지 않는 청크를 근거처럼 반환하지 않는다.
|
||||
- 검색 결과가 0건이면 `문서 없음`으로 실패한다.
|
||||
- LLM이 컨텍스트만으로 답변 불가로 판단하면 `failure_reason`을 채우고, 성공처럼 반환하지 않는다.
|
||||
- 수치형 질문에서 값이 없으면 추정하지 않는다.
|
||||
- 내부 규정이나 최신 집계가 없으면 `문서 없음`, `미확인`, `불일치` 중 하나로 명시한다.
|
||||
- 메타 대화로 회피하지 않는다.
|
||||
@ -40,7 +46,7 @@ tags: [workflow, rag, companyx, grounding, answer]
|
||||
## 현재 기준
|
||||
- 이 흐름은 `rb8001` 답변 합성 규칙과 연결된다.
|
||||
- 검색 결과를 그대로 붙이는 방식은 허용하지 않는다.
|
||||
- 질문 유형별 분기와 근거 적합도 평가가 필요하다.
|
||||
- 질문 유형 분류는 LLM 프롬프트 톤 조절용으로만 사용한다.
|
||||
|
||||
## 검증 기준
|
||||
- `오늘전통/옐로펀치` 같은 기준 질문에서 직접 답 + 근거 문서가 함께 나와야 한다.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user