IV. AI Agent가 LLM의 응답을 JSON으로 받고, 그 안에서 Tool 이름과 인자(arguments)를 꺼내서 실제 Tool 실행으로 연결하기 위한 기초
#-------- 1. JSON 문자열을 Python 객체로.
import json
raw = '{"tool":"search", "args":{"query":"refund policy"}, "done":false}'
data = json.loads(raw)
print(type(data))
print(data["tool"])
print(data["args"]["query"])
print(data["done"], type(data["done"]))
#-------- 2. Python 객체를 JSON 문자열로
import json
payload = {
"model":"some-model",
"max_tokens": 2048,
"messages":[{"role":"user", "content":"Hello!"}]
}
compact = json.dumps(payload)
print(compact)
print(type(compact))
#-------- 3. 복잡한 API 응답. indent=2
import json
response = {
"id":"msg_014",
"stop_reason":"tool_use",
"usage":{"input_tokens":302, "output_tokens":47,},
"content":[{"type":"tool_use", "name":"calculator", "input":{"expr":"18*4"}}]
}
print(json.dumps(response, indent=2))
#-------- 4. Text와 Tool이 함께 있을 수 있다
response = {
"stop_reason":"tool_use",
"content":[
{"type":"text", "text":"나는 그것을 계산할거야."},
{"type":"tool_use", "name":"calculator", "input":{"expr":"24*4"}},
],
}
blocks = response.get("content", [])
tool_calls = [b for b in blocks if b.get("type")=="tool_use"]
text_parts = [b.get("text", "") for b in blocks if b.get("type")=="text"]
print("Model said: ", "".join(text_parts))
if tool_calls:
call = tool_calls[0]
print("Tool wanted: ", call.get("name"))
print("Arguments: ", call.get("input",{}))
#-------- 5. 지저분한 LLM 응답에서 JSON 찾기
import json
messy = """
분명히 여기에 결과값이 있어.
```json
{"tool":"search", "args":{"query":"개점 시각"}}
```
추가로 필요한 사항이 있으면 나에게 알려줘.
"""
start = messy.find("{")
end = messy.rfind("}")
if start != -1 and end != -1:
candidate = messy[start:end+1]
data = json.loads(candidate)
print("Parsed: ", data)
print("Query: ", data["args"]["query"])
else:
print("JSON 객체가 확인되지 않습니다.")
import json
replies = [
'{"tool":"search", "args":{}}',
'{"tool":"search", "args":{},}',
'우리가 그것을 찾아야 한다고 나는 생각해.'
]
for reply in replies:
try:
data = json.loads(reply)
print(f"OK -> {data}")
except json.JSONDecodeError as e:
print(f"FAIL -> {e.msg} at position {e.pos}")
# OK -> {'tool': 'search', 'args': {}}
# FAIL -> Illegal trailing comma before end of object at position 27
# FAIL -> Expecting value at position 0
#-------- 6. JSON 오류 처리
import json
def parse_reply(text):
result = {"ok":False, "tool":None, "args":{}, "error":None}
if not text or not text.strip():
result["error"] = "빈 응답입니다..."
return result
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1:
result["error"] = "JSON 객체가 확인되지 않습니다..."
return result
try:
data = json.loads(text[start:end+1])
except json.JSONDecodeError as e:
result["error"] = f"invalid JSON: {e.msg}"
return result
result["ok"] = True
result["tool"] = data.get("tool")
result["args"] = data.get("args", {})
return result
samples = [
'```json\n{"tool":"search", "args":{"query":"현재 시간"}}\n```',
'완료되었습니다. 더이상 툴이 필요없습니다..',
'{"tool":"calculator",}',
'',
]
for s in samples:
print(parse_reply(s))
# {'ok': True, 'tool': 'search', 'args': {'query': '현재 시간'}, 'error': None}
# {'ok': False, 'tool': None, 'args': {}, 'error': 'JSON 객체가 확인되지 않습니다...' }
# {'ok': False, 'tool': None, 'args': {}, 'error': 'invalid JSON: Illegal trailing comma before end of object'}
# {'ok': False, 'tool': None, 'args': {}, 'error': '빈 응답입니다...'}
1. 먼저 JSON이 무엇인지
코드에 계속 나오는:
{"tool":"search", "args":{"query":"refund policy"}, "done":false}
이것부터 이해해야 합니다.
JSON은 데이터를 구조적으로 표현하는 문자열 형식입니다.
사람이 보면:
tool = search
query = refund policy
done = false
라는 의미입니다.
Python으로 표현하면:
{
"tool": "search",
"args": {
"query": "refund policy"
},
"done": False
}
와 거의 같은 구조입니다.
중요한 차이는:
JSON
↓
문자열
Python dict
↓
실제 Python 객체
라는 것입니다.
2. json.loads() — JSON 문자열을 Python 객체로
첫 번째 코드입니다.
import json
raw = '{"tool":"search", "args":{"query":"refund policy"}, "done":false}'
data = json.loads(raw)
여기서:
json.loads()
가 핵심입니다.
loads는:
JSON 문자열을 Python 객체로 변환
합니다.
즉:
JSON 문자열
↓
json.loads()
↓
Python dictionary
입니다.

3. 왜 loads라고 부르는가?
조금 헷갈릴 수 있습니다.
json.loads()
의 s는 string을 의미한다고 생각하면 편합니다.
즉:
loads
↓
load string
입니다.
반면 나중에 나오는:
json.dumps()
는 Python 객체를 JSON 문자열로 바꿉니다.
Python dict
↓
json.dumps()
↓
JSON 문자열
따라서 두 개를 세트로 기억하면 좋습니다.
json.loads()
JSON → Python
json.dumps()
Python → JSON
4. type(data)
print(type(data))
json.loads()를 거쳤으므로 data는 Python dictionary가 됩니다.
결과:
<class 'dict'>
입니다.
즉:
raw
는 문자열이지만,
data
는 실제 Python dict입니다.
5. data["tool"]
print(data["tool"])
JSON에서:
"tool":"search"
부분을 꺼냅니다.
결과:
search
입니다.
6. data["args"]["query"]
이 부분이 중요합니다.
print(data["args"]["query"])
구조를 단계적으로 보면:
data
↓
data["args"]
↓
{
"query": "refund policy"
}
↓
data["args"]["query"]
↓
refund policy
입니다.
즉:
data 안의 args 안의 query를 가져온다.
는 뜻입니다.
AI Agent에서 아주 흔하게 나오는 형태입니다.
예를 들어 LLM이:
{
"tool": "search",
"args": {
"query": "서울 날씨"
}
}
라고 하면 Agent는:
tool = data["tool"]
query = data["args"]["query"]
를 통해:
tool
↓
search
query
↓
서울 날씨
를 알아낼 수 있습니다.
7. JSON의 false와 Python의 False
print(data["done"], type(data["done"]))
원래 JSON에는:
"done":false
라고 되어 있습니다.
Python에서는:
False
입니다.
즉 json.loads()가 JSON의 자료형을 Python 자료형으로 변환합니다.
대표적으로:
| "hello" | str |
| 123 | int |
| 12.5 | float |
| true | True |
| false | False |
| null | None |
| {} | dict |
| [] | list |
이 대응 관계는 꼭 기억해두세요.
8. json.dumps() — Python 객체를 JSON 문자열로
두 번째 부분입니다.
payload = {
"model":"some-model",
"max_tokens":2048,
"messages":[
{
"role":"user",
"content":"Hello!"
}
]
}
이것은 Python dictionary입니다.
그리고:
compact = json.dumps(payload)
를 합니다.
즉:
Python dict
↓
json.dumps()
↓
JSON 문자열
입니다.
9. 결과는 문자열이다
print(compact)
print(type(compact))
결과는 대략:
{"model": "some-model", "max_tokens": 2048, "messages": [{"role": "user", "content": "Hello!"}]}
<class 'str'>
입니다.
여기서 중요한 포인트:
payload
→ dict
compact
→ str
입니다.
즉:
Python 객체
↓ dumps
JSON 문자열
입니다.
10. loads와 dumps를 확실히 구분하기
이건 앞으로 AI API 코드를 볼 때 정말 중요합니다.
json.dumps()
Python dict ─────────────→ JSON 문자열
json.loads()
JSON 문자열 ─────────────→ Python dict
쉽게 외우면:
dumps = Python 데이터를 밖으로 내보낼 문자열로 만든다.
loads = 문자열을 Python 안으로 불러온다.
11. 세 번째 부분: 복잡한 API 응답
response = {
"id":"msg_014",
"stop_reason":"tool_use",
"usage":{
"input_tokens":302,
"output_tokens":47,
},
"content":[
{
"type":"tool_use",
"name":"calculator",
"input":{
"expr":"18*4"
}
}
]
}
이것은 LLM의 응답을 흉내 낸 데이터입니다.
구조를 보면:
response
│
├── id
│ └── msg_014
│
├── stop_reason
│ └── tool_use
│
├── usage
│ ├── input_tokens
│ └── output_tokens
│
└── content
└── [0]
├── type
│ └── tool_use
├── name
│ └── calculator
└── input
└── expr
└── 18*4
입니다.
12. indent=2
print(json.dumps(response, indent=2))
여기서 indent=2가 중요합니다.
그냥:
json.dumps(response)
하면 한 줄로 나옵니다.
{"id": "msg_014", "stop_reason": "tool_use", ...}
그런데:
json.dumps(response, indent=2)
하면 사람이 읽기 좋게 들여쓰기를 합니다.
{
"id": "msg_014",
"stop_reason": "tool_use",
"usage": {
"input_tokens": 302,
"output_tokens": 47
},
"content": [
{
"type": "tool_use",
"name": "calculator",
"input": {
"expr": "18*4"
}
}
]
}
AI API 응답을 디버깅할 때 아주 유용합니다.
13. stop_reason: "tool_use"
이 부분은 Agent에서 매우 중요합니다.
"stop_reason":"tool_use"
라고 되어 있습니다.
이것은 단순히:
"AI가 말을 끝냈다."
가 아니라,
"AI가 Tool을 사용하기 위해 응답을 끝냈다."
라는 의미로 이해하면 됩니다.
즉 Agent는 이런 신호를 보고:
stop_reason
↓
tool_use
↓
Tool 실행 필요
라고 판단할 수 있습니다.
14. Tool 호출 정보
content 안에:
{
"type":"tool_use",
"name":"calculator",
"input":{
"expr":"18*4"
}
}
가 있습니다.
이것은 사실상:
사용할 Tool
↓
calculator
Tool에 전달할 값
↓
expr = "18*4"
입니다.
Agent가 이것을 해석하면:
calculator("18*4")
같은 실제 함수 호출로 연결할 수 있습니다.
15. 네 번째 부분: Text와 Tool이 함께 있을 수 있다
이번에는:
response = {
"stop_reason":"tool_use",
"content":[
{
"type":"text",
"text":"나는 그것을 계산할거야."
},
{
"type":"tool_use",
"name":"calculator",
"input":{
"expr":"24*4"
}
},
],
}
입니다.
여기서 중요한 것은 content에 두 종류의 블록이 들어 있다는 것입니다.
content
│
├── [0] text
│ "나는 그것을 계산할거야."
│
└── [1] tool_use
calculator
expr = 24*4
즉 LLM이:
- 사람에게 설명하고
- Tool도 요청할 수 있습니다.
16. .get()
blocks = response.get("content", [])
여기서:
dict.get()
을 사용했습니다.
일반적으로:
response["content"]
라고 하면 content가 없을 경우 오류가 발생합니다.
반면:
response.get("content", [])
는:
content가 있으면 가져오고, 없으면 빈 리스트 []를 사용해라.
라는 의미입니다.
따라서 좀 더 안전합니다.
17. Tool 호출만 골라내기
tool_calls = [
b for b in blocks
if b.get("type")=="tool_use"
]
이것은 앞에서 배운 list comprehension입니다.
쉽게 풀어쓰면:
tool_calls = []
for b in blocks:
if b.get("type") == "tool_use":
tool_calls.append(b)
와 같습니다.
즉:
blocks 중에서 type이 tool_use인 것만 골라라.
입니다.
결과:
[
{
"type":"tool_use",
"name":"calculator",
"input":{
"expr":"24*4"
}
}
]
가 됩니다.
18. Text만 골라내기
text_parts = [
b.get("text", "")
for b in blocks
if b.get("type")=="text"
]
이번에는 반대로:
type == "text"
인 블록만 골라냅니다.
결과:
["나는 그것을 계산할거야."]
가 됩니다.
19. "".join(text_parts)
print("Model said: ", "".join(text_parts))
join()은 여러 문자열을 하나로 합칩니다.
예:
parts = ["나는 ", "계산할게.", " 잠시만요."]
이라면:
"".join(parts)
→
나는 계산할게. 잠시만요.
입니다.
따라서 LLM이 여러 개의 text block으로 응답해도 하나의 문자열로 만들 수 있습니다.
20. Tool이 있는지 검사
if tool_calls:
Python에서는 리스트가 비어 있으면 False, 내용이 있으면 True처럼 취급됩니다.
즉:
[]
→ False
[{"type":"tool_use", ...}]
→ True
입니다.
따라서:
if tool_calls:
는 사실상:
Tool 호출 요청이 하나라도 있으면
이라는 의미입니다.
21. 첫 번째 Tool 호출 가져오기
call = tool_calls[0]
Tool 호출이 여러 개 있을 수 있으므로 그중 첫 번째 것을 가져옵니다.
그리고:
print("Tool wanted: ", call.get("name"))
→
Tool wanted: calculator
그리고:
print("Arguments: ", call.get("input",{}))
→
Arguments: {'expr': '24*4'}
가 됩니다.
Agent 입장에서 보면:
LLM
↓
calculator Tool을 써라
↓
expr = 24*4
↓
calculator("24*4")
입니다.
22. 다섯 번째: 지저분한 LLM 응답에서 JSON 찾기
이 부분이 실제 Agent 개발에서 매우 중요합니다.
messy = """
분명히 여기에 결과값이 있어.
```json
{"tool":"search", "args":{"query":"개점 시각"}}
추가로 필요한 사항이 있으면 나에게 알려줘.
"""
LLM이 항상 JSON만 딱 출력해준다는 보장이 없습니다.
이렇게 말할 수도 있습니다.
```text
분명히 여기에 결과값이 있어.
```json
{"tool":"search", "args":{"query":"개점 시각"}}
추가로 필요한 사항이 있으면 나에게 알려줘.
우리가 원하는 것은 가운데 JSON입니다.
---
# 23. `.find("{")`
```python
start = messy.find("{")
find()는 문자열에서 특정 문자가 처음 등장하는 위치를 찾습니다.
예를 들어:
text = "abc{hello}"
라면:
text.find("{")
→ {의 위치를 반환합니다.
찾지 못하면:
-1
을 반환합니다.
24. .rfind("}")
end = messy.rfind("}")
rfind()의 r은 right, 즉 뒤쪽에서부터 찾는다는 의미입니다.
따라서 마지막 }의 위치를 찾습니다.
결국:
messy
↓
첫 번째 {
↓
JSON 시작 위치
마지막 }
↓
JSON 끝 위치
를 찾는 것입니다.
25. candidate
candidate = messy[start:end+1]
여기서 +1이 중요합니다.
Python slicing은 마지막 인덱스를 포함하지 않습니다.
예를 들어:
text[2:5]
이면:
2, 3, 4
까지만 가져옵니다.
따라서 마지막 }까지 포함하려면:
end + 1
을 해야 합니다.
결국:
candidate
에는:
{"tool":"search", "args":{"query":"개점 시각"}}
만 들어갑니다.
26. json.loads(candidate)
이제 깨끗한 JSON만 분리했으므로:
data = json.loads(candidate)
를 할 수 있습니다.
그리고:
data["args"]["query"]
를 통해:
개점 시각
을 얻습니다.
27. start != -1 and end != -1
if start != -1 and end != -1:
은:
{도 찾았고 }도 찾았는가?
를 확인하는 것입니다.
둘 중 하나라도 못 찾으면 JSON 객체가 없다고 판단합니다.
28. 여섯 번째: JSON 오류 처리
다음 코드는 실제로 상당히 좋은 연습입니다.
replies = [
'{"tool":"search", "args":{}}',
'{"tool":"search", "args":{},}',
'우리가 그것을 찾아야 한다고 나는 생각해.'
]
세 가지 응답이 있습니다.
첫 번째
{"tool":"search", "args":{}}
정상적인 JSON입니다.
두 번째
{"tool":"search", "args":{},}
마지막에 불필요한 ,가 있습니다.
JSON에서는 허용되지 않습니다.
세 번째
우리가 그것을 찾아야 한다고 나는 생각해.
JSON 자체가 아닙니다.
29. try/except JSONDecodeError
for reply in replies:
try:
data = json.loads(reply)
print(f"OK -> {data}")
except json.JSONDecodeError as e:
print(f"FAIL -> {e.msg} at position {e.pos}")
정상적인 JSON이면:
json.loads(reply)
가 성공합니다.
그러면:
OK -> ...
를 출력합니다.
하지만 JSON 형식이 잘못되면:
JSONDecodeError
가 발생합니다.
그것을 except에서 잡습니다.
30. e.msg와 e.pos
e.msg
→ 오류 메시지
e.pos
→ 오류가 발생한 문자열 위치
입니다.
그래서:
FAIL -> Illegal trailing comma before end of object at position 27
같이 표시할 수 있습니다.
이런 방식은 LLM이 예상과 다른 형식으로 응답했을 때 원인을 찾는 데 매우 유용합니다.
31. 마지막 parse_reply() 함수
이제 지금까지 배운 것들을 하나의 함수로 합쳤습니다.
def parse_reply(text):
이 함수의 목적은:
LLM의 응답 문자열을 받아서 Tool 이름과 arguments를 안전하게 추출하는 것
입니다.
이 함수는 Agent를 만들 때 매우 중요한 개념입니다.
32. 기본 결과 구조
result = {
"ok":False,
"tool":None,
"args":{},
"error":None
}
처음에는 일단 실패했다고 가정합니다.
ok
↓
False
Tool은 아직 모르므로:
tool
↓
None
args도 아직 없으므로:
args
↓
{}
error도 아직 없으므로:
error
↓
None
입니다.
이런 방식을 기본값을 먼저 만들어 놓고 성공하면 수정하는 방식이라고 생각하면 됩니다.
33. 빈 응답 검사
if not text or not text.strip():
두 가지를 검사합니다.
not text
아예 값이 없는 경우.
not text.strip()
공백만 있는 경우.
예:
""
또는:
" "
를 잡아냅니다.
그러면:
result["error"] = "빈 응답입니다..."
return result
로 끝냅니다.
34. JSON 위치 찾기
start = text.find("{")
end = text.rfind("}")
앞에서 배웠던 것과 같습니다.
LLM 응답 전체에서:
첫 번째 {
와
마지막 }
를 찾습니다.
35. 여기에는 작은 버그가 하나 있다
다음 코드를 보세요.
if start == -1 or end == -1:
result["error"] == "JSON 객체가 확인되지 않습니다..."
return result
여기:
result["error"] == "JSON 객체가 확인되지 않습니다..."
는 오타입니다.
==는 비교 연산자입니다.
즉:
a == b
는:
a와 b가 같은가?
를 묻는 것입니다.
값을 넣으려면:
=
를 사용해야 합니다.
따라서 올바른 코드는:
result["error"] = "JSON 객체가 확인되지 않습니다..."
입니다.
이건 상당히 중요한 차이입니다.
= → 대입
== → 비교
36. JSON parsing
try:
data = json.loads(text[start:end+1])
except json.JSONDecodeError as e:
result["error"] = f"invalid JSON: {e.msg}"
return result
JSON을 읽어봅니다.
성공하면:
data
에 Python dictionary가 들어갑니다.
실패하면:
result["error"]
에 오류 내용을 저장하고 종료합니다.
37. 성공하면 결과를 채운다
result["ok"] = True
JSON을 성공적으로 읽었으므로 성공 표시를 합니다.
그리고:
result["tool"] = data.get("tool")
Tool 이름을 가져옵니다.
예:
"tool":"search"
→
result["tool"]
→ "search"
입니다.
38. data.get("args", {})
result["args"] = data.get("args", {})
여기서도 .get()을 사용합니다.
args가 있으면 가져오고:
args
↓
{"query":"현재 시간"}
없으면:
args
↓
{}
를 사용합니다.
그래서 안전합니다.
39. 마지막 테스트
samples = [
'```json\n{"tool":"search", "args":{"query":"현재 시간"}}\n```',
'완료되었습니다. 더이상 툴이 필요없습니다..',
'{"tool":"calculator",}',
'',
]
네 가지 상황을 테스트합니다.
① JSON이 Markdown 코드블록 안에 있음
```json
{"tool":"search", "args":{"query":"현재 시간"}}
→ `{`와 `}`를 찾아 JSON을 추출할 수 있습니다.
결과:
```python
{
"ok": True,
"tool": "search",
"args": {
"query": "현재 시간"
},
"error": None
}
② JSON이 아예 없음
완료되었습니다. 더이상 툴이 필요없습니다..
{와 }가 없습니다.
따라서 실패합니다.
그런데 앞에서 말씀드린 == 버그 때문에 실제 실행 결과에서는:
"error": None
으로 남습니다.
수정하면:
result["error"] = "JSON 객체가 확인되지 않습니다..."
가 되어야 합니다.
즉 예상되는 정상적인 결과는:
{
"ok": False,
"tool": None,
"args": {},
"error": "JSON 객체가 확인되지 않습니다..."
}
입니다.
③ 잘못된 JSON
{"tool":"calculator",}
마지막 , 때문에 JSON 파싱이 실패합니다.
따라서:
{
"ok": False,
"tool": None,
"args": {},
"error": "invalid JSON: ..."
}
가 됩니다.
④ 빈 문자열
''
이므로:
if not text or not text.strip():
에 걸립니다.
결과:
{
"ok": False,
"tool": None,
"args": {},
"error": "빈 응답입니다..."
}
입니다.
40. 이 코드를 Agent의 관점에서 보면
이번 코드의 진짜 목적은 이것입니다.
LLM이 다음과 같이 답했다고 가정합니다.
{
"tool": "calculator",
"args": {
"expr": "24*4"
}
}
Agent는 이것을 받아서:
① JSON인가?
↓
② JSON parsing
↓
③ tool = calculator
↓
④ args = {"expr":"24*4"}
↓
⑤ calculator 실행
↓
⑥ 결과를 history에 추가
↓
⑦ 다시 LLM 호출
하는 것입니다.
즉 LLM은 직접 Python 함수를 실행하는 것이 아니라, "어떤 Tool을 어떤 인자로 사용할지"를 구조화해서 알려주고 Agent 프로그램이 실제 실행하는 것입니다.
41. 지금까지 배운 Agent 구조와 연결
지금까지 공부하신 내용을 모두 연결하면 상당히 재미있습니다.
┌─────────────────────┐
│ User │
│ "18 × 4 계산해줘" │
└──────────┬──────────┘
↓
history 저장
↓
┌─────────────────────┐
│ System Prompt │
│ Tool 사용 규칙 │
└──────────┬──────────┘
↓
LLM 호출
↓
┌─────────────────────┐
│ LLM 응답 │
│ │
│ tool: calculator │
│ expr: 18*4 │
└──────────┬──────────┘
↓
JSON parsing
↓
tool / args 추출
↓
┌─────────────────────┐
│ calculator │
│ 18*4 │
└──────────┬──────────┘
↓
72
↓
history 추가
↓
LLM 재호출
↓
최종 답변
이게 바로 Tool을 사용하는 Agent의 기본적인 Loop입니다.
42. 이번 코드에서 꼭 기억해야 할 것
이번 코드는 양이 많지만 핵심은 크게 5개입니다.
① json.loads()
data = json.loads(raw)
JSON 문자열 → Python 객체
② json.dumps()
text = json.dumps(data)
Python 객체 → JSON 문자열
③ .get()
data.get("args", {})
key가 없어도 오류가 나지 않도록 기본값을 지정
④ list comprehension
tool_calls = [
b for b in blocks
if b.get("type") == "tool_use"
]
많은 데이터 중 원하는 것만 골라내기
⑤ try / except JSONDecodeError
try:
data = json.loads(text)
except json.JSONDecodeError:
...
LLM의 잘못된 JSON 응답에 대비
43. 그리고 이번 코드에서 발견되는 중요한 버그 2개
마지막으로 꼭 짚고 넘어가겠습니다.
버그 1 — ==를 =로 수정
현재:
result["error"] == "JSON 객체가 확인되지 않습니다..."
수정:
result["error"] = "JSON 객체가 확인되지 않습니다..."
버그 2 — parse_reply()의 JSON 유효성 검사 한계
현재 방식:
start = text.find("{")
end = text.rfind("}")
candidate = text[start:end+1]
은 교육용으로는 좋지만 실제 Agent에서는 완벽하지 않습니다.
예를 들어 LLM이:
여기 JSON이 있습니다.
{"tool":"search", "args":{"query":"{테스트}"}}
끝입니다.
처럼 중첩된 {}나 문자열 안에 {}를 넣으면 단순히 첫 {와 마지막 }를 찾는 방법만으로는 문제가 생길 수 있습니다.
또한 JSON이 두 개 들어 있어도 애매해질 수 있습니다.
따라서 실제 Agent에서는 가능하면 LLM API가 제공하는 구조화 출력(structured output)이나 공식 Tool Calling 기능을 이용하는 것이 훨씬 안전합니다.
🌟 이번 코드를 한 문장으로 정리하면
**"LLM이 반환한 복잡한 응답에서 JSON을 안전하게 해석하고, tool과 args를 추출해서 실제 Python Tool 실행으로 연결하기 위한 준비 코드"**입니다.
그리고 지금까지 공부하신 것 중에서 history와 이번 JSON/tool parsing을 합치면 Agent의 핵심 구조가 거의 보이기 시작합니다.
history
↓
LLM
↓
response
↓
JSON / tool_use 분석
↓
tool + args
↓
Tool 실행
↓
결과
↓
history
↓
LLM
이 순환이 바로 앞으로 공부하실 Agent Loop의 핵심입니다.