ToolReplay를 사용한 AI 에이전트 도구 호출 기록 감사 및 분석
요약
ToolReplay는 AI 에이전트의 도구 호출 기록을 감사하고 분석하는 도구입니다. 이 도구는 세션이 비결정적이었는지, 중복된 호출이 있었는지, 또는 선언된 권한 범위를 벗어났는지 등을 보고하여 에이전트의 신뢰성과 재현 가능성을 검증합니다. 특히 결정론적 재실행을 통해 불일치 지점(divergence)과 발견 사항(findings)을 식별하는 것이 핵심입니다.
핵심 포인트
- ToolReplay는 에이전트 도구 호출 기록 감사에 사용됩니다.
- 비결정성, 중복 호출, 권한 범위 이탈 등을 감지합니다.
- 재현 가능성을 잃는 지점(divergence)을 정확히 찾아냅니다.
- Python 3.11 이상에서 외부 종속성 없이 실행 가능합니다.
ToolReplay는 AI 에이전트의 도구(tool) 호출이 기록된 트랜스크립트를 감사하고, 해당 세션이 비결정적(non-deterministic)이었는지, 중복되었는지, 또는 선언된 권한 범위를 벗어났는지 보고합니다.
현재 이 도구는 이 저장소에 포함된 더티 샘플(dirty sample)을 사용하여 작동하는 모습입니다. 명령어는 출력 상단에 표시되며, 출력은 이 체크아웃에서 실행된 내용을 그대로 붙여넣은 것입니다:
$ python -m toolreplay replay samples/session-dirty.jsonl
calls: 6
divergence: index 5
...
이 세 개의 헤더 라인과 두 가지 발견 사항(findings)이 여섯 번의 호출로 이루어진 세션 전체 감사 내용입니다. 위에서 아래로 읽어보세요.
calls: 6
은 samples/session-dirty.jsonl에서 파싱된 레코드의 개수입니다. 파싱은 엄격하므로, 여기서 6이라는 숫자는 인덱스 0부터 5까지의 잘 구성된(well-formed) 라인 6개를 의미하며 결함이 없는 상태를 뜻합니다.
divergence: index 5
는 결정론적 재실행(deterministic re-run)을 했을 때 기록과 불일치하는 첫 번째 인덱스입니다. 이 도구는 인덱스 5에서 이전 호출을 반복했지만 다른 기록된 응답을 가지고 있었다는 것을 발견했으므로, 세션이 재현 가능성을 잃는 가장 빠른 지점입니다.
findings: 2
은 헤더 아래의 감사 결과를 계산한 것입니다. 이 결과들은 인덱스별로 정렬하고 종류(kind)별로 정렬하여 고정된 순서로 출력되므로, 동일한 입력은 항상 바이트 단위로 동일한 출력을 생성합니다.
첫 번째 발견 사항은 인덱스 2에 있는 중복 호출입니다: docs/intro.md에 대한 read_file 호출이 이미 인덱스 1에서 이루어졌으며, 두 호출 사이에는 파일 내용을 변경할 수 있는 것이 없었으므로, 두 번째 읽기는 새로운 작업을 수행하지 않았습니다. 두 번째 발견 사항은 인덱스 5에 있는 비결정성입니다: 동일한 호출(install에 대한 search)이 인덱스 3에서는 3개의 결과를 반환하고 인덱스 5에서는 7개의 결과를 반환했습니다. 프로세스는 발견 사항이 존재했기 때문에 종료 코드 1로 종료되었습니다.
권한 범위(scope) 확인은 별도의 명령어입니다. 왜냐하면 scope는 replay가 필요로 하지 않는, 선언된 권한 파일(declared permission file)을 필요로 하기 때문입니다. 동일한 세션과 포함된 scope 파일을 대상으로 실행했을 때, 에이전트의 선언된 도구 범위를 벗어난 한 호출을 발견합니다:
$ python -m toolreplay scope samples/session-dirty.jsonl samples/scope.json
calls: 6
findings: 1
...
Replay와 Scope 간에, 이 도구가 감지하는 세 가지 유형의 모든 발견 사항은 해당 여섯 줄 샘플에서 모두 나타납니다.
toolreplay는 Python 3.11 이상이 필요하며 외부 서드파티 런타임 종속성이 없습니다. 네트워크 접근을 수행하지 않습니다. 소스 체크아웃에서 바로 실행할 수 있습니다:
python -m toolreplay version
또는 콘솔 스크립트를 설치하고 이름으로 호출할 수 있습니다:
pip install .
toolreplay version
둘 다 동일한 줄을 출력합니다:
$ python -m toolreplay version
toolreplay 0.6.0
| 명령어 | 기능 | 읽는 파일 |
|---|---|---|
seal <transcript> | 해시 체인된 봉인된 트랜스크립트를 JSONL로 출력합니다 | 하나의 트랜스크립트 |
replay <transcript> | 비결정성(non-determinism), 중복 호출, 그리고 분기(divergence)를 보고합니다 | 하나의 트랜스크립트 |
verify <sealed> | 체인을 재계산하고 첫 번째 깨진 연결 고리를 보고합니다 | 봉인된 파일 |
scope <transcript> <scope> | 모든 호출을 선언된 범위 파일과 비교하여 확인합니다 | 트랜스크립트 + 범위 |
version | 버전을 출력합니다 | 없음 |
replay와 scope는 의도적으로 분리되어 있습니다. Replay는 세션을 자체적으로 판단하며 외부 입력이 필요 없습니다. Scope는 세션을 공급하는 권한 선언(permission declaration)과 비교하여 판단하므로 두 번째 파일이 필요합니다. 이 둘을 분리함으로써, 범위 파일이 없는 세션도 재실행할 수 있고, 세션이 깨끗하게 재실행되었는지 여부에 관계없이 범위를 확인할 수 있습니다.
각 발견 유형은 규칙(rule), samples/session-dirty.jsonl에서 가져온 실제 예시(real example), 그리고 트랜스크립트가 사람이 아닌 에이전트로부터 왔을 때 중요한 이유(reason it matters)를 가지고 있습니다.
규칙: 호출이 처음 나타날 때, 기록된 응답이 기억됩니다. 동일한 호출(동일한 도구 이름, 표준 JSON 인코딩 후 동일한 인자)이 다른 기록된 응답과 함께 다시 나타나면, 그것은 비결정성입니다. 첫 번째 그러한 인덱스가 분기 지점(divergence point)이 됩니다.
실제 예시: 인덱스 3과 인덱스 5 모두 install에 대한 search입니다. 인덱스 3은 `{
. 동일한 질문에 대해 두 개의 답변을 받는 경우, 도구는 인덱스 5에서 비결정성(non-determinism)을 보고하고 이를 발산 지점(divergence point)으로 표시합니다.
이것이 에이전트에게 중요한 이유: 같은 질문을 두 번 하고 두 개의 답변을 받은 에이전트는 신뢰성 있게 재현하거나 디버깅할 수 없습니다. 첫 번째 답변을 사용한 단계가 두 번째 답변이었다면 달라졌을 결정을 내렸을 수도 있습니다. 비결정성은 세션의 결과가 기록된 입력 외부에 의존한다는 신호입니다.
규칙: 두 개의 동일한 호출은 그 사이에 상태를 변경할 수 있는 것이 아무것도 없었을 때만 중복(redundant)으로 간주됩니다. 호출이 가능한 상태 변화는 해당 도구가 변형자(mutator)인 경우, 또는 반복된 호출과 다른 모든 호출인 경우입니다. 기본 변형자는 write_file, delete_file, create_file, move_file, 그리고 run_command입니다. 이는 의도적으로 보수적입니다: 중복을 발명하는 것보다 놓치는 것을 선호합니다.
실제 예시: 인덱스 1과 인덱스 2는 모두 docs/intro.md에 대한 read_file이며, 서로 인접해 있고 그 사이에 아무것도 없습니다. 두 번째 읽기는 첫 번째가 알지 못한 것을 배우지 못했으므로, 인덱스 2는 중복으로 플래그 지정됩니다. 파일 수준에서의 대조를 주목하십시오: 인덱스 4는 동일한 파일을 쓰기 때문에, 인덱스 4 이후의 나중 읽기는 세계(world)를 변경했을 수 있으므로 중복이 아닙니다.
이것이 에이전트에게 중요한 이유: 중복 호출은 낭비되는 토큰과 지연 시간이며, 종종 에이전트가 이미 알고 있던 것을 놓쳤다는 것을 의미합니다. 한 번의 반복은 저렴합니다. 반복 루프는 예산을 소모하는 막힌(stuck) 에이전트를 만듭니다.
규칙: 각 호출의 도구 이름은 스코프 파일에 있는 allowed_tools 목록과 비교되며, 정확하고 대소문자를 구분하여 일치해야 합니다. 목록에 없는 모든 도구는 과도한 접근(overreach)입니다. read_file에 대해 Read_File 같은 근접 일치를 조용히 수락하는 스코프는 스코프가 아니므로, 일치는 엄격합니다.
실제 예시: samples/scope.json은 에이전트 docs-reader에게 read_file, list_dir, 그리고 search를 허용합니다. 인덱스 4는 write_file을 호출하는데, 이는 목록에 없으므로 docs-reader에 대한 과도한 접근으로 보고됩니다.
에이전트에게 중요한 이유: 파일을 작성하는 읽기 전용 에이전트가 프롬프트 주입(prompt injection), 계획 오류(planning error) 또는 잘못 구성된 도구 세트를 통해 부여받은 권한을 초과했습니다. 과도한 권한 사용(Overreach)은 보안 질문, 즉 이 에이전트가 허용된 작업만 수행했는지에 직접적으로 연결되는 감사 결과입니다.
전사 기록(transcript)은 JSON Lines 파일입니다. 비어 있지 않은 각 줄은 하나의 도구 호출이며, 정확히 네 개의 필드를 가지고 있고 다른 필드는 없는 JSON 객체입니다.
| 필드 | 타입 | 의미 |
|---|---|---|
index | integer | 세션 내 위치로, 0부터 시작하여 정확히 1씩 증가합니다 |
tool | string | 호출된 도구의 이름 |
args | object | 도구에 전달된 인자(arguments) |
response | object | 기록된 도구가 반환한 응답 |
파싱은 엄격하며, 이 엄격함이 핵심입니다. 알 수 없는 필드, 누락된 필수 필드, 비정수형 인덱스, 객체가 아닌 args 또는 response, 또는 순서가 맞지 않는 인덱스는 심각한 오류(hard error)입니다. Python에서 true는 int의 서브클래스이므로 정수가 필요한 곳에 부울(boolean) 값을 허용할 수 없습니다. 따라서 입력이 실제로 무엇을 말했는지 보고할 수 없는, 조용히 입력을 수정하는 감사 도구는 신뢰할 수 없으므로 대신 거부합니다.
두 호출은 표준 호출 문자열(canonical call string)이 일치할 때 동일한 것으로 간주됩니다. 이 표준 호출 문자열은 키가 정렬되고 부수적인 공백이 없는 {"tool": ..., "args": ...}의 JSON 인코딩입니다. 따라서 {"x": 1, "y": 2}와 {"y": 2, "x": 1}는 동일한 호출입니다. 표준 응답(canonical response)도 같은 방식으로 인코딩되는데, 이것이 느슨한 비교가 아닌 정확하게 비결정성(non-determinism)을 감지하는 방법입니다.
여기에 오염된 샘플에서 나온 실제 한 줄, 즉 과도한 권한 사용을 유발하는 쓰기 호출이 있습니다:
{"index": 4, "tool": "write_file", "args": {"path": "docs/intro.md", "text": "edited"}, "response": {"ok": true}}
seal은 이 전사 기록을 링크 체인으로 변환합니다. N번째 링크의 다이제스트(digest)는 이전 다이제스트에 레코드 N의 표준 바이트(its prev, index)를 더한 SHA-256 해시 값입니다.
, tool
,
args
, and response
(키는 정렬되어 인코딩됨). genesis 링크의 이전 다이제스트는 64개의 0으로 된 16진수 문자입니다. 각 다이제스트가 그 앞에 있는 것을 포함하기 때문에, 더 이른 기록을 변경하면 모든 나중 다이제스트가 변경됩니다.
$ python -m toolreplay seal samples/session-dirty.jsonl
{"args":{"path":"docs"},"digest":"c1bd7fb3e28ce29e5c9dbd0cf47cafcaa1613295be26d86fb04463fe3d8b40da","index":0,"prev":"0000000000000000000000000000000000000000000000000000000000000000","response":{"entries":["intro.md","guide.md"]},"tool":"list_dir"}
...
verify는 체인을 재계산하고 링크당 두 가지를 확인합니다: 저장된 이전 다이제스트가 그 앞 링크의 다이제스트와 일치하는지, 그리고 저장된 다이제스트가 기록으로부터 재계산된 다이제스트와 일치하는지입니다. 손상되지 않은 체인은 그렇게 보고 0으로 종료됩니다:
$ python -m toolreplay verify sealed.jsonl
chain: intact
임의로 기록된 응답을 변조하고 재검증하면, 체인은 더 이상 일치하지 않는 첫 번째 링크를 보고합니다. 이 실행은 검증 전에 인덱스 3에서 `
에이전트(agent): "docs-reader", 허용 도구(allowed_tools): ["read_file", "list_dir", "search"]
누락된 agent,
누락된 allowed_tools,
비문자열(non-string)의 agent, 비배열(non-array)의 tool 목록, 또는 목록 내의 비문자열 항목은 치명적인 오류(hard error)입니다. 전사 기록(transcript)과 마찬가지로, 파서는 추측하기보다는 잘못된 범위(malformed scope)를 거부합니다.
모든 명령어는 타임스탬프나 무작위성이 없는 라인별 보고서(line-oriented report)를 출력하므로, 동일한 입력에 대해 두 번 실행해도 차이가 없습니다.
replay 보고서는 세 줄의 헤더와 발견된 항목당 한 줄로 구성됩니다:
| 라인 | 의미 |
|---|---|
calls: N | 파싱된 레코드 수 |
divergence: ... | none, 또는 첫 비결정성(non-determinism)의 경우 index N |
findings: N | 뒤따르는 발견 항목(finding) 개수 |
| finding lines | index N: <kind>: <detail> 형식으로, 인덱스별, 종류별로 정렬됨 |
scope 보고서는 범위는 재실행 순서에 대한 개념이 없기 때문에 비결정성 라인(divergence)을 생략합니다:
| 라인 | 의미 |
|---|---|
calls: N | 파싱된 레코드 수 |
findings: N | 범위 초과(overreach) 발견 항목 개수 |
| finding lines | index N: permission-overreach: <detail> |
verify 보고서는 단일 라인 chain: intact이거나, 봉인(sealing) 섹션에 표시된 첫 번째 끊어진 링크와 예상 및 발견된 다이제스트를 명시하는 네 줄로 구성됩니다.
| 코드 | 의미 |
|---|---|
| 0 | 깨끗함(Clean): 발견 항목 없음, 또는 온전한 체인(intact chain) |
| ... |
1과 2 사이의 구분은 자동화에서 중요합니다. 종료 코드 1은 도구가 실행되었고 보고할 내용이 있다는 의미입니다. 종료 코드 2는 파일 누락이나 전사 기록 파싱 불가 등으로 인해 도구를 실행할 수 없었다는 의미이므로, 빌드 시스템에서는 이 두 가지를 다르게 처리해야 합니다.
명령어들은 발견 항목이 있을 경우 비영(non-zero)으로 종료되므로, 직접 게이트(gate) 역할을 수행합니다. 에이전트 전사 기록이 분기하거나 범위를 초과할 때 빌드를 실패시키는 단계는 단순히 해당 명령어 그 자체입니다:
python -m toolreplay replay session.jsonl
python -m toolreplay scope session.jsonl scope.json
보고서는 결정론적(deterministic)이며 타임스탬프가 없기 때문에, 봉인된 기록(sealed transcript)을 커밋하고 두 실행(run)의 차이점(diff)을 git으로 확인할 수도 있습니다. 각 실행을 파일로 봉인한 다음 비교합니다:
python -m toolreplay seal run-a.jsonl > run-a.sealed.jsonl
python -m toolreplay seal run-b.jsonl > run-b.sealed.jsonl
git --no-pager diff --no-index run-a.sealed.jsonl run-b.sealed.jsonl
다르게 나타나는 첫 번째 줄이 두 실행이 동의를 멈춘 첫 호출이며, 그 다이제스트(digest)가 변경되었다는 것은 이후 모든 줄도 변경되었음을 알려줍니다.
이는 실제적이고 의도된 것입니다. 이 도구는 볼 수 없는 것에 대해서는 정직합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 GitHub AI Tools의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기