hee_!J commited on
Commit
0969faa
ยท
1 Parent(s): 26084d8

feat(rag): CRAG self-correction (retrieval grader + query refiner)

Browse files
Files changed (2) hide show
  1. agents/rag/crag.py +202 -0
  2. agents/tools/knowledge.py +29 -11
agents/rag/crag.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CRAG (Corrective Retrieval-Augmented Generation)
2
+
3
+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๊ด€๋ จ์„ฑ์„ LLM์ด ์ž์ฒด ํ‰๊ฐ€ํ•˜๊ณ , ์ž„๊ณ„์น˜ ๋ฏธ๋‹ฌ ์‹œ ์ฟผ๋ฆฌ๋ฅผ ์žฌ์ž‘์„ฑํ•ด ์žฌ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
4
+ Yan et al., 2024 "Corrective Retrieval Augmented Generation" ํŒจํ„ด์„ ๋ณธ ๋„๋ฉ”์ธ์— ์ ์‘.
5
+
6
+ ํ๋ฆ„:
7
+ 1. base retrieval (hybrid)
8
+ 2. **grade**: ๊ฐ ๋ฌธ์„œ๊ฐ€ ์ฟผ๋ฆฌ์— ๊ด€๋ จ ์žˆ๋Š”์ง€ LLM์ด 0~1 ์ ์ˆ˜ ๋ถ€์—ฌ
9
+ 3. avg_score >= THRESHOLD ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
10
+ 4. ๋ฏธ๋งŒ์ด๋ฉด **refine**: LLM์ด ๋™์˜์–ดยท๊ด€๋ จ ๋„๋ฉ”์ธ ์šฉ์–ด๋ฅผ ํ™œ์šฉํ•ด ์ฟผ๋ฆฌ ์žฌ์ž‘์„ฑ
11
+ 5. ์žฌ๊ฒ€์ƒ‰ (max_retries ๊นŒ์ง€)
12
+
13
+ ๋น„์šฉ ์ตœ์†Œํ™”๋ฅผ ์œ„ํ•ด grader/refiner๋Š” gpt-4o-mini ์‚ฌ์šฉ.
14
+ ํ™˜๊ฒฝ๋ณ€์ˆ˜ CRAG_ENABLED=false๋กœ ์ „์ฒด ๋น„ํ™œ์„ฑ ๊ฐ€๋Šฅ (์‹คํ—˜ ๋น„๊ต์šฉ).
15
+ """
16
+ import json
17
+ import os
18
+
19
+ from agents.llm import client
20
+ from agents.rag.store import load_document, search
21
+
22
+ GRADER_MODEL = "gpt-4o-mini"
23
+ RELEVANCE_THRESHOLD = 0.5 # avg ์ ์ˆ˜ ๋ฏธ๋งŒ์ด๋ฉด refinement ์‹œ๋„
24
+ DEFAULT_MAX_RETRIES = 1
25
+
26
+ _GRADE_SCHEMA = {
27
+ "type": "object",
28
+ "properties": {
29
+ "grades": {
30
+ "type": "array",
31
+ "items": {
32
+ "type": "object",
33
+ "properties": {
34
+ "index": {"type": "integer"},
35
+ "score": {"type": "number"},
36
+ "reason": {"type": "string"},
37
+ },
38
+ "required": ["index", "score", "reason"],
39
+ "additionalProperties": False,
40
+ },
41
+ }
42
+ },
43
+ "required": ["grades"],
44
+ "additionalProperties": False,
45
+ }
46
+
47
+
48
+ def _llm_call(prompt: str, schema: dict | None = None):
49
+ kwargs = {
50
+ "model": GRADER_MODEL,
51
+ "messages": [{"role": "user", "content": prompt}],
52
+ "temperature": 0,
53
+ }
54
+ if schema:
55
+ kwargs["response_format"] = {
56
+ "type": "json_schema",
57
+ "json_schema": {"name": "out", "schema": schema, "strict": True},
58
+ }
59
+ return client().chat.completions.create(**kwargs)
60
+
61
+
62
+ def grade_retrieval(query: str, docs: list[dict]) -> list[dict]:
63
+ """๊ฐ ๋ฌธ์„œ์˜ query ๊ด€๋ จ์„ฑ์„ 0~1๋กœ ์ฑ„์ 
64
+
65
+ docs: [{"doc_id": str, "snippet": str}, ...]
66
+ ๋ฐ˜ํ™˜: [{"index": int, "score": float, "reason": str}, ...]
67
+ """
68
+ if not docs:
69
+ return []
70
+ doc_block = "\n\n".join(
71
+ f"[doc_{i}] (id={d['doc_id']})\n{d['snippet'][:600]}" for i, d in enumerate(docs)
72
+ )
73
+ prompt = f"""๋‹น์‹ ์€ ๋ฐ˜๋„์ฒด ๊ณต์ • ๋„๋ฉ”์ธ์˜ retrieval ํ‰๊ฐ€์ž์ž…๋‹ˆ๋‹ค.
74
+ ๋‹ค์Œ ์ฟผ๋ฆฌ์— ๋Œ€ํ•ด ๊ฐ ๋ฌธ์„œ๊ฐ€ ๋‹ต๋ณ€ ์ƒ์„ฑ์— ์–ผ๋งˆ๋‚˜ ์ง์ ‘ ๊ธฐ์—ฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ‰๊ฐ€ํ•˜์„ธ์š”.
75
+
76
+ [์ฟผ๋ฆฌ]
77
+ {query}
78
+
79
+ [๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค]
80
+ {doc_block}
81
+
82
+ ๊ฐ ๋ฌธ์„œ์— ๋Œ€ํ•ด 0~1 ์ ์ˆ˜(์†Œ์ˆ˜ ๋‘˜์งธ ์ž๋ฆฌ)์™€ ํ•œ ์ค„ reason์„ JSON์œผ๋กœ ์‘๋‹ต:
83
+ {{"grades": [
84
+ {{"index": 0, "score": 0.85, "reason": "์ฟผ๋ฆฌ์˜ ํ•ต์‹ฌ ์ฆ์ƒยท์›์ธ์„ ์ง์ ‘ ๊ธฐ์ˆ "}},
85
+ ...
86
+ ]}}
87
+
88
+ [์ฑ„์  ๊ธฐ์ค€]
89
+ - 0.0: ์ฟผ๋ฆฌ์™€ ๋ฌด๊ด€, ๋˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์˜๋ฏธ ๋ถˆ๋ช…/๋ฌด์ž‘์œ„ ์ž…๋ ฅ
90
+ - 0.1~0.3: ๋„๋ฉ”์ธ์€ ๊ฐ™์œผ๋‚˜ ๋‹ค๋ฅธ ์ฃผ์ œ (์˜ˆ: ์ฟผ๋ฆฌ๊ฐ€ Photo์ธ๋ฐ ๋ฌธ์„œ๋Š” CMP)
91
+ - 0.4~0.6: ์ธ์ ‘ ์ฃผ์ œ ๋˜๋Š” ์ผ๋ฐ˜๋ก  (์ง์ ‘ ๋‹ต์€ ์•ˆ ๋˜์ง€๋งŒ ๋งฅ๋ฝ์€ ๋จ)
92
+ - 0.7~0.9: ์ง์ ‘ ๊ด€๋ จ (๊ตฌ์ฒด์  ์‚ฌ๋ก€ยทSOPยท๊ทผ๊ฑฐ)
93
+ - 1.0: ์ฟผ๋ฆฌ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ๋ฅผ ๋ชจ๋‘ ํฌํ•จํ•˜๊ณ  ๋‹ต๋ณ€ ์ƒ์„ฑ์— ์ง์ ‘ ๊ธฐ์—ฌ
94
+
95
+ [์ค‘์š”]
96
+ - ์ฟผ๋ฆฌ๊ฐ€ ์˜๋ฏธ ๋ถˆ๋ช…ยท๋ฌด์ž‘์œ„ ๋‹จ์–ดยท๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ด๋ฉด ๋ชจ๋“  ๋ฌธ์„œ์— 0.0 ๋ถ€์—ฌ
97
+ - ๋„๋ฉ”์ธ(๋ฐ˜๋„์ฒด ๊ณต์ •)์ด ๊ฐ™๋‹ค๋Š” ์ด์œ ๋งŒ์œผ๋กœ ์ ์ˆ˜๋ฅผ ๋†’์ด์ง€ ๋งˆ์„ธ์š”
98
+ - ๋ณด์ˆ˜์ ์œผ๋กœ ์ฑ„์ ํ•˜์„ธ์š” (์˜์‹ฌ์Šค๋Ÿฌ์šฐ๋ฉด ๋‚ฎ์€ ์ ์ˆ˜)"""
99
+ try:
100
+ resp = _llm_call(prompt, schema=_GRADE_SCHEMA)
101
+ parsed = json.loads(resp.choices[0].message.content)
102
+ return parsed.get("grades", [])
103
+ except (json.JSONDecodeError, KeyError):
104
+ return [{"index": i, "score": 0.5, "reason": "(grader parse failed)"} for i in range(len(docs))]
105
+
106
+
107
+ def refine_query(original_query: str, weak_docs: list[dict]) -> str:
108
+ """์•ฝํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ์ฟผ๋ฆฌ๋ฅผ ์žฌ์ž‘์„ฑ"""
109
+ weak_block = "\n".join(
110
+ f"- [{d['doc_id']}] {d['snippet'][:200]}" for d in weak_docs
111
+ )
112
+ prompt = f"""์› ์ฟผ๋ฆฌ๊ฐ€ ์ ์ ˆํ•œ ๋ฌธ์„œ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๋” ์ž˜ ์ž‘๋™ํ•  ์ฟผ๋ฆฌ๋กœ ํ•œ ์ค„ ์žฌ์ž‘์„ฑํ•˜์„ธ์š”.
113
+
114
+ [์› ์ฟผ๋ฆฌ]
115
+ {original_query}
116
+
117
+ [๊ฒ€์ƒ‰๋œ (๊ด€๋ จ์„ฑ ๋‚ฎ์€) ๋ฌธ์„œ๋“ค]
118
+ {weak_block}
119
+
120
+ [์žฌ์ž‘์„ฑ ๊ทœ์น™]
121
+ - ๋™์˜์–ดยท๊ด€๋ จ ๋„๋ฉ”์ธ ์šฉ์–ด ํ™œ์šฉ (์˜ˆ: '๋ Œ์ฆˆ ์˜ค์—ผ' โ†’ 'ํ—ค์ด์ฆˆ, ๊ด‘ํ•™ ํ‘œ๋ฉด ์˜ค์—ผ, projection lens contamination')
122
+ - ๋„ˆ๋ฌด ์ข๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ๋„“์ง€ ์•Š๊ฒŒ ์œ ์ง€
123
+ - ํ•œ๊ตญ์–ด + ์˜์–ด ๋„๋ฉ”์ธ ์šฉ์–ด ํ˜ผ์šฉ ๊ฐ€๋Šฅ
124
+ - ์•ฝ 10~25 ๋‹จ์–ด
125
+
126
+ ์žฌ์ž‘์„ฑ๋œ ์ฟผ๋ฆฌ๋งŒ ํ•œ ์ค„๋กœ ๋‹ตํ•˜์„ธ์š” (๋‹ค๋ฅธ ์„ค๋ช… ์—†์ด):"""
127
+ resp = _llm_call(prompt)
128
+ content = resp.choices[0].message.content or ""
129
+ return content.strip().splitlines()[0] if content.strip() else original_query
130
+
131
+
132
+ def crag_search(
133
+ query: str,
134
+ top_k: int = 3,
135
+ max_retries: int = DEFAULT_MAX_RETRIES,
136
+ trace_list: list | None = None,
137
+ ) -> dict:
138
+ """CRAG: search โ†’ grade โ†’ (๋‚ฎ์œผ๋ฉด) refine โ†’ re-search
139
+
140
+ ๋ฐ˜ํ™˜: {"hits": [{"doc_id", "snippet", "relevance_score"}, ...],
141
+ "crag_meta": {"retries": int, "final_query": str, "final_avg_score": float}}
142
+ """
143
+ current_query = query
144
+ retries = 0
145
+ last_docs: list[dict] = []
146
+ last_grades: list[dict] = []
147
+ avg_score = 0.0
148
+
149
+ while True:
150
+ doc_ids = search(current_query, top_k=top_k)
151
+ docs = []
152
+ for d in doc_ids:
153
+ text = load_document(d)
154
+ if not text:
155
+ continue
156
+ docs.append({"doc_id": d, "snippet": text[:600] + ("..." if len(text) > 600 else "")})
157
+
158
+ if not docs:
159
+ break
160
+
161
+ grades = grade_retrieval(current_query, docs)
162
+ avg_score = sum(g.get("score", 0.0) for g in grades) / max(len(grades), 1)
163
+ last_docs = docs
164
+ last_grades = grades
165
+
166
+ if trace_list is not None:
167
+ trace_list.append({
168
+ "query": current_query,
169
+ "retry": retries,
170
+ "avg_score": round(avg_score, 3),
171
+ "doc_ids": [d["doc_id"] for d in docs],
172
+ "grades": [{"id": docs[g["index"]]["doc_id"], "score": g["score"]}
173
+ for g in grades if g.get("index", -1) < len(docs)],
174
+ })
175
+
176
+ if avg_score >= RELEVANCE_THRESHOLD or retries >= max_retries:
177
+ break
178
+
179
+ # ๊ด€๋ จ์„ฑ ๋‚ฎ์Œ โ†’ query refinement
180
+ current_query = refine_query(current_query, docs)
181
+ retries += 1
182
+
183
+ # docs์™€ grades ์ •๋ ฌ (index ๊ธฐ์ค€)
184
+ score_by_idx = {g["index"]: g.get("score", 0.0) for g in last_grades}
185
+ hits = [
186
+ {"doc_id": d["doc_id"], "snippet": d["snippet"], "relevance_score": round(score_by_idx.get(i, 0.0), 2)}
187
+ for i, d in enumerate(last_docs)
188
+ ]
189
+ return {
190
+ "hits": hits,
191
+ "crag_meta": {
192
+ "retries": retries,
193
+ "final_query": current_query,
194
+ "final_avg_score": round(avg_score, 3),
195
+ "refined": retries > 0,
196
+ },
197
+ }
198
+
199
+
200
+ def crag_enabled() -> bool:
201
+ """ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ CRAG on/off ํ† ๊ธ€ (๊ธฐ๋ณธ on)"""
202
+ return os.getenv("CRAG_ENABLED", "true").lower() not in ("false", "0", "no")
agents/tools/knowledge.py CHANGED
@@ -1,22 +1,39 @@
1
- """search_knowledge tool - RAG ๊ฒ€์ƒ‰์„ LLM์ด ์ž์œจ ํ˜ธ์ถœ
2
 
3
- ๊ธฐ์กด agents.rag.store.search()๋ฅผ tool ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋…ธ์ถœ
4
- LLM์ด ์ถ”๊ฐ€ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•  ๋•Œ ํ˜ธ์ถœ
 
 
5
  """
 
6
  from agents.rag.store import load_document, search
7
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  def search_knowledge(query: str, top_k: int = 3) -> dict:
10
- """์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ(INC/FMEA/SOP/FLOW)๋ฅผ ์˜๋ฏธ + ํ‚ค์›Œ๋“œ hybrid ๊ฒ€์ƒ‰
11
 
12
- ๋ฐ˜ํ™˜: {"hits": [{"doc_id": ..., "snippet": ์ฒซ 400์ž}, ...]}
13
  """
 
 
 
 
14
  doc_ids = search(query, top_k=top_k)
15
  hits = []
16
- for doc_id in doc_ids:
17
- text = load_document(doc_id)
18
- snippet = text[:400] + ("..." if len(text) > 400 else "")
19
- hits.append({"doc_id": doc_id, "snippet": snippet})
20
  return {"hits": hits}
21
 
22
 
@@ -26,8 +43,9 @@ SCHEMA = {
26
  "name": "search_knowledge",
27
  "description": (
28
  "์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ(๊ณผ๊ฑฐ ์‚ฌ๋ก€ INC, ์‹คํŒจ ๋ชจ๋“œ FMEA, ํ‘œ์ค€ ์ ˆ์ฐจ SOP, ๊ณต์ • ํ๋ฆ„ FLOW)๋ฅผ "
29
- "hybrid ๊ฒ€์ƒ‰ํ•ด ๊ด€๋ จ ๋ฌธ์„œ์˜ doc_id์™€ ๋ณธ๋ฌธ ์š”์•ฝ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. "
30
- "์›์ธ ๋ถ„์„ยท๋Œ€์‘ ๊ถŒ๊ณ ์— ํ•„์š”ํ•œ ๋„๋ฉ”์ธ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์‚ฌ์šฉํ•˜์„ธ์š”."
 
31
  ),
32
  "parameters": {
33
  "type": "object",
 
1
+ """search_knowledge tool - RAG ๊ฒ€์ƒ‰ (CRAG self-correction ํฌํ•จ)
2
 
3
+ ๊ธฐ๋ณธ์€ CRAG ํ™œ์„ฑํ™” (๊ฒ€์ƒ‰ โ†’ ๊ด€๋ จ์„ฑ ํ‰๊ฐ€ โ†’ ๋ฏธ๋‹ฌ ์‹œ ์ฟผ๋ฆฌ ์žฌ์ž‘์„ฑ + ์žฌ๊ฒ€์ƒ‰).
4
+ ํ™˜๊ฒฝ๋ณ€์ˆ˜ CRAG_ENABLED=false ๋กœ ๋น„ํ™œ์„ฑ ๊ฐ€๋Šฅ (์‹คํ—˜ ๋น„๊ต์šฉ).
5
+
6
+ ์ „์—ญ trace list (`LAST_CRAG_TRACE`)์— CRAG ๋ฉ”ํƒ€๊ฐ€ ๋ˆ„์ ๋˜์–ด agent ํ˜ธ์ถœ๋ณ„ ๊ด€์ฐฐ ๊ฐ€๋Šฅ.
7
  """
8
+ from agents.rag.crag import crag_enabled, crag_search
9
  from agents.rag.store import load_document, search
10
 
11
+ # ํ˜ธ์ถœ ๋‹จ์œ„๋กœ reset ํ›„ agent loop ๋™์•ˆ CRAG ๋™์ž‘์„ ์ถ”์ 
12
+ LAST_CRAG_TRACE: list[dict] = []
13
+
14
+
15
+ def reset_crag_trace() -> list[dict]:
16
+ """์ด์ „ trace ํšŒ์ˆ˜ ํ›„ ์ƒˆ๋กœ ์‹œ์ž‘ - ์‹คํ—˜ยทagent trace ์ˆ˜์ง‘์šฉ"""
17
+ global LAST_CRAG_TRACE
18
+ out = LAST_CRAG_TRACE
19
+ LAST_CRAG_TRACE = []
20
+ return out
21
+
22
 
23
  def search_knowledge(query: str, top_k: int = 3) -> dict:
24
+ """์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ๋ฅผ hybrid ๊ฒ€์ƒ‰ + CRAG ์ž์ฒด ํ‰๊ฐ€
25
 
26
+ CRAG ํ™œ์„ฑ ์‹œ ๋ฐ˜ํ™˜์— relevance_score, refined ์—ฌ๋ถ€ ํฌํ•จ.
27
  """
28
+ if crag_enabled():
29
+ result = crag_search(query, top_k=top_k, trace_list=LAST_CRAG_TRACE)
30
+ return result
31
+ # CRAG ๋น„ํ™œ์„ฑ: ๊ธฐ์กด hybrid search ๊ทธ๋Œ€๋กœ
32
  doc_ids = search(query, top_k=top_k)
33
  hits = []
34
+ for d in doc_ids:
35
+ text = load_document(d)
36
+ hits.append({"doc_id": d, "snippet": text[:400] + ("..." if len(text) > 400 else "")})
 
37
  return {"hits": hits}
38
 
39
 
 
43
  "name": "search_knowledge",
44
  "description": (
45
  "์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ(๊ณผ๊ฑฐ ์‚ฌ๋ก€ INC, ์‹คํŒจ ๋ชจ๋“œ FMEA, ํ‘œ์ค€ ์ ˆ์ฐจ SOP, ๊ณต์ • ํ๋ฆ„ FLOW)๋ฅผ "
46
+ "hybrid ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. CRAG self-correction์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๊ด€๋ จ์„ฑ์ด "
47
+ "๋‚ฎ์œผ๋ฉด ์ž๋™์œผ๋กœ ์ฟผ๋ฆฌ๋ฅผ ์žฌ์ž‘์„ฑํ•ด ์žฌ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜ ๊ฐ’์— relevance_score(0~1)๊ฐ€ ํฌํ•จ๋˜์–ด "
48
+ "์›์ธ ๋ถ„์„ยท๋Œ€์‘ ๊ถŒ๊ณ ์— ์‹ ๋ขฐ๋„์™€ ํ•จ๊ป˜ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
49
  ),
50
  "parameters": {
51
  "type": "object",