정답 코드 보기 (solution.py)
#!/usr/bin/env python3
"""Lab 3 — 에이전트 루프: 호출 → 실행 → 덧붙이기 → 반복.
Lab 2는 도구를 한 번만 허용했습니다. 실제 작업은 "파일 목록 보기 → 읽기 → 고치기 →
테스트" 처럼 여러 번 필요하죠. 해법은 while 루프 하나입니다.
"에이전트"란 결국 stop_reason 이 tool_use 인 동안 도는 이 루프입니다.
실행: python labs/03_agent_loop/solution.py
"""
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from common.brain import get_brain # noqa: E402
from common.toolbox import BASE_TOOLS, execute_tool # noqa: E402
SYSTEM = ("너는 로컬 머신에서 일하는 엔지니어링 에이전트다. "
"도구로 작업을 끝낸 뒤 한 일을 요약하라.")
MAX_ROUNDS = 50
def run_turn(brain, messages, tools=BASE_TOOLS, system=SYSTEM,
execute=execute_tool, max_rounds=MAX_ROUNDS, log=print):
"""사용자 한 턴 = 작업이 끝날 때까지 필요한 만큼의 모델/도구 왕복.
규칙
1. 매 라운드: 두뇌 호출 → 응답 content 전체를 assistant 메시지로 추가
2. stop_reason 이 "tool_use" 가 아니면 → 응답 텍스트를 반환하고 끝
3. 응답 속 '모든' tool_use 를 실행 → tool_result 들을 '하나의' user 메시지로 추가
4. max_rounds 를 넘으면 멈추고 "[harness]" 로 시작하는 문자열 반환 (폭주 방지)
"""
# >>> TODO: 위 규칙대로 루프를 구현하세요 (log 로 도구 호출을 출력하면 보기 좋습니다)
for _ in range(max_rounds):
response = brain({"system": system, "messages": messages, "tools": tools})
messages.append({"role": "assistant", "content": response["content"]})
if response["stop_reason"] != "tool_use":
return "".join(b["text"] for b in response["content"] if b["type"] == "text")
results = []
for b in response["content"]:
if b["type"] == "tool_use":
log(f" [tool] {b['name']}({str(b['input'])[:100]})")
results.append({"type": "tool_result", "tool_use_id": b["id"],
"content": execute(b["name"], b["input"])})
messages.append({"role": "user", "content": results})
return f"[harness] MAX_ROUNDS({max_rounds}) 도달 — 이번 턴을 중단합니다."
# <<< TODO
def main():
brain, messages = get_brain(), []
while True:
try:
user_text = input("\nyou> ").strip()
except EOFError:
break
if user_text in ("/quit", ""):
break
messages.append({"role": "user", "content": user_text})
print(f"\nassistant> {run_turn(brain, messages)}")
if __name__ == "__main__":
main()
채점 테스트 보기 (test_lab.py)
import os
import sys
import unittest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from common.labkit import load_target # noqa: E402
from common.brain import ScriptedBrain, reply, text, tool_use # noqa: E402
lab = load_target(__file__)
quiet = lambda *a, **k: None # noqa: E731
def fake_exec(name, args):
return f"ran {name} {args.get('command', '')}"
class Lab3(unittest.TestCase):
def run_script(self, script, **kw):
brain = ScriptedBrain(script)
msgs = [{"role": "user", "content": "작업해줘"}]
out = lab.run_turn(brain, msgs, execute=fake_exec, log=quiet, **kw)
return brain, msgs, out
def test_plain_answer_one_call(self):
brain, msgs, out = self.run_script([reply(text("그냥 답"))])
self.assertEqual(out, "그냥 답")
self.assertEqual(len(brain.requests), 1)
self.assertEqual(len(msgs), 2)
def test_multi_round(self):
brain, msgs, out = self.run_script([
reply(tool_use("run_command", {"command": "ls"}, id="t1")),
reply(text("생각 중"), tool_use("run_command", {"command": "cat x"}, id="t2")),
reply(text("끝!")),
])
self.assertEqual(out, "끝!")
self.assertEqual(len(brain.requests), 3)
self.assertEqual([m["role"] for m in msgs],
["user", "assistant", "user", "assistant", "user", "assistant"])
self.assertEqual(msgs[2]["content"][0]["tool_use_id"], "t1")
self.assertEqual(msgs[4]["content"][0]["content"], "ran run_command cat x")
# 세 번째 요청엔 앞의 모든 왕복이 담겨 있어야 한다
self.assertEqual(len(brain.requests[2]["messages"]), 5)
self.assertIn("tools", brain.requests[0])
def test_parallel_tools_one_user_message(self):
brain, msgs, out = self.run_script([
reply(tool_use("run_command", {"command": "a"}, id="p1"),
tool_use("run_command", {"command": "b"}, id="p2")),
reply(text("ok")),
])
results = msgs[2]["content"]
self.assertEqual([r["tool_use_id"] for r in results], ["p1", "p2"],
"병렬 tool_use 결과는 '하나의' user 메시지에 모두 담아야 합니다")
def test_max_rounds(self):
forever = [reply(tool_use("run_command", {"command": "again"})) for _ in range(5)]
brain, msgs, out = self.run_script(forever, max_rounds=3)
self.assertEqual(len(brain.requests), 3)
self.assertTrue(out.startswith("[harness]"))
if __name__ == "__main__":
unittest.main()