AI 에이전트 — 모델이 아니라 그래프 · 도구 호출과 결과 검증 · 실습
인자를 지어낸 한 번의 호출
목표
도구를 부르는 쪽의 규율을 코드로 적는다. 계약 표를 한 곳에 두고, 고른 인자를 부르기 전에 재고, 돌려받은 결과를 믿지 않고 재고, 실패를 예외가 아니라 값으로 다뤄 사유를 상태에 남기고, 그 사유로 다음 시도를 고치고, 같은 인자로 두 번 부르지 않게 기억한다.
왜 중요한가
도구를 붙인 에이전트가 처음 깨지는 자리는 모델이 아니라 호출 경계다. 모델이 인자 이름을 하나 지어내면 TypeError 가 그래프 밖으로 튀어나와 실행 전체가 끝나고, 상태도 지나온 길도 남지 않는다. 더 나쁜 쪽은 예외가 나지 않는 경우다 — 도구가 빈 목록을 돌려줬는데 그 위에 "재고 3곳" 이라는 문장이 만들어진다.
그래서 이 실습은 계약을 값으로 적는 것에서 시작한다. 이름·인자 스키마·돌려주는 모양·실제 함수를 표 하나에 묶어 두면, 재는 코드가 도구마다 필요 없고 부르는 문이 한 곳으로 좁아진다. 그 문 앞에서 인자를 재고, 문 뒤에서 결과를 재고, 문 앞에 기억을 둔다.
인자를 고르는 자리는 이 실습에서 규칙으로 대신합니다. 실제 에이전트는 그 자리(pick 노드)에서 모델에게 도구 이름과 인자를 묻습니다. 나머지 구조는 똑같습니다 — 오히려 모델이 고를수록 부르기 전 검증이 더 필요합니다.
채점기는 여러분이 적어 둔 설명을 믿지 않습니다. 여러분의 모듈을 실제로 불러 검증 함수를 매번 다른 값으로 두드려 보고, 도구 몸통이 몇 번 돌았는지를 세어 "부르기 전에 막았는가" 를 확인합니다. sku·지역·개수는 실행마다 바뀝니다.
단계
1. /root/work/agtool/tools.py 에 자료(REGIONS·STOCK)와 MAX_ROWS·CALLS, 도구 둘(stock_lookup·legacy_stock), 계약 표 TOOLS 를 만드세요.
2. validate_args(name, args) 를 만들어 부르기 전에 인자를 재게 하세요. 통과하면 빈 문자열, 아니면 사유 하나를 돌려줍니다.
3. call_tool(name, args) 를 만들어 검증을 통과한 호출만 실제로 부르게 하세요. 예외는 밖으로 내지 않고 {"ok", "value", "error"} 로 돌려줍니다.
4. validate_result(name, value) 를 만들고 call_tool 이 결과도 재게 하세요. 사유는 shape·empty·too_big·range 입니다.
5. CACHE 와 cache_key(name, args) 를 만들어 같은 도구를 같은 인자로 두 번 부르지 않게 하세요. 성공만 기억합니다.
6. State 와 노드 넷(pick·fetch·reply·giveup), build_naive()·run_naive(order) 를 만들어 실패를 예외가 아니라 경로로 다루세요.
7. MAX_ATTEMPTS·REPAIRS·can_repair·repair 노드와 build_graph()·handle(order) 를 더해 사유를 보고 고쳐 다시 시도하게 하세요.
8. /root/work/agtool/tool_report.json 과 /root/work/agtool/tool_report.md 에 확인한 것을 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/agtool/tools.py를 파이썬 모듈로 불러REGIONS·STOCK·MAX_ROWS·CALLS·TOOLS·stock_lookup·legacy_stock·validate_args·validate_result·call_tool·CACHE·cache_key·run_naive·MAX_ATTEMPTS·REPAIRS·can_repair·handle을 직접 씁니다. 스크립트로 실행하지 않으므로if __name__ == "__main__"은 없어도 됩니다. - 이 실습의 자료는 이렇게 씁니다.
REGIONS = {"seoul": ["gasan", "guro", "mapo"], "busan": ["sasang", "haeundae"], "jeju": ["hallim"]},STOCK = {"A-1001": {"gasan": 4, "guro": 2, "mapo": 7, "sasang": 1}, "A-1002": {"guro": 5, "haeundae": 3}, "B-2001": {"gasan": 9, "guro": 1, "mapo": 9, "hallim": 2}, "B-2002": {"sasang": 6, "haeundae": 6}},MAX_ROWS = 2. - 두 도구의 서명은
(sku, region, limit)로 같고, 돌려주는 것도{"sku": 문자열, "rows": [{"warehouse": 문자열, "count": 정수}, ...]}로 같습니다.rows는 개수 내림차순, 같으면 창고 이름 오름차순입니다.stock_lookup은limit개까지만 돌려주고,legacy_stock은limit을 무시하고 그 지역의 창고를 전부 돌려줍니다(옛 판 흉내). CALLS는 도구 몸통이 돈 횟수를 이름별로 세는 딕셔너리입니다. 두 도구의 첫 줄에서 1씩 올리세요. 채점기가 이 값으로 "부르기 전에 막았는가" 를 확인합니다.TOOLS한 항목의 모양:{"fn": 함수, "args": {인자이름: {"type": "str"|"int", "required": True, "allowed": [...], "min": N, "max": N}}, "returns": {"sku": {"type": "str"}, "rows": {"type": "list", "max_len": MAX_ROWS, "item": {"warehouse": {"type": "str"}, "count": {"type": "int", "min": 0}}}}}.region의allowed는REGIONS의 열쇠들이고,limit은min1 ·maxMAX_ROWS입니다.validate_args의 사유:unknown_tool·missing:<이름>·extra:<이름>·type:<이름>·allowed:<이름>·range:<이름>. 통과하면 빈 문자열입니다. 채점기는 결함이 하나뿐인 인자만 던집니다.validate_result의 사유:shape(모양·형이 다르다) ·empty(rows가 비었다) ·too_big(rows가max_len보다 길다) ·range(count가 계약의 범위 밖이다). 통과하면 빈 문자열입니다.call_tool이 돌려주는error는 앞에 출처를 붙입니다: 인자 쪽은args:<사유>, 결과 쪽은result:<사유>, 도구가 예외를 던졌으면raised:<예외이름>. 성공이면 빈 문자열입니다.handle(order)의order는{"sku": ..., "region": ..., "limit": ..., "prefer": ...}입니다.prefer가 있으면 그 도구를, 없으면stock_lookup을 씁니다. 답은{"ok", "answer", "rows", "errors", "fixes", "attempts", "calls", "tool_runs"}이고tool_runs는 이 건을 처리하는 동안 도구 몸통이 돈 횟수입니다.REPAIRS는{"args:allowed:region": "region", "args:range:limit": "limit", "result:too_big": "tool"}입니다. 지역은FALLBACK_REGION("seoul")으로, 개수는MAX_ROWS로, 도구는stock_lookup으로 고칩니다.MAX_ATTEMPTS는 3 입니다.- 노드 이름과 상태 열쇠는 같은 이름 공간입니다. 상태에
tool열쇠를 두고 노드도tool이라고 부르면ValueError: 'tool' is already being used as a state key로 컴파일이 죽습니다. - 이 파드에는 인터넷이 없습니다.
pip install은 되지 않습니다. langgraph 0.2.60 이 이미 들어 있습니다(python3 -c "import langgraph"). - 공식 문서: [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents) · [Use the graph API](https://docs.langchain.com/oss/python/langgraph/use-graph-api) · [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api)
- 흔한 실수: 부르고 나서 인자를 재기(몸통이 이미 돌았습니다),
isinstance(True, int)가 참이라True를 개수로 받기, 빈 결과를 성공으로 넘기기, 실패까지 기억해 고쳐도 다시 못 부르기, 시도 상한 없이 고치고 또 고치기.
단계 8개
- 계약을 값으로 한 곳에 적는다
- 부르기 전에 인자를 잰다
- 잘못된 인자로는 부르지 않는다
- 돌려준 것을 믿지 않는다
- 같은 것을 두 번 묻지 않는다
- 실패를 경로로 다룬다
- 사유를 보고 고쳐 다시 시도한다
- 재어 본 것을 기록한다