마지막 업데이트 2026-09-18
한 줄 요약. Phoenix는 이 프로젝트의 유일한 화면이다. 정본 데이터는 agent-trace의 SQLite에 있고, Phoenix는 그 데이터를 trace 트리·세션 흐름·주석으로 보여주는 뷰어다. 이 문서는 agent-trace 실행 가이드의 "4. Phoenix에서 보기" 한 단락을 화면 단위로 풀어 쓴 것이다.
가장 자주 쓰는 것. ① 필터칸에 agent.tool == 'codex' and turn.latency_ms > 300000처럼 점 이름을 그대로 쓴다. attributes['…'] 형태는 에러 없이 0건이 나오니 쓰지 않는다. ② 룰·judge 결과는 annotations['rule:tool_loop'].score >= 3처럼 주석 이름으로 건다. ③ 이상한 턴을 보면 루트 span에 HUMAN 주석을 남기고 터미널에서 sync-marks → judge --push를 돌린다.
Arize Phoenix는 LLM 애플리케이션용 오픈소스 관측 서버다. OTLP로 span을 받아 저장하고, 웹 UI에서 trace·세션 단위로 탐색하며, span마다 사람·코드·LLM이 남긴 주석(annotation)을 붙일 수 있다. 라이선스는 Elastic License 2.0으로, 로컬 개인 사용에는 제약이 없다(관리형 서비스로 재판매만 금지).
| 항목 | 값 |
|---|---|
| 실행 | docker compose -f deploy/phoenix/docker-compose.yml up -d (agent-trace 저장소 안) |
| 이미지 / 컨테이너 | arizephoenix/phoenix:version-20.11.0 / agent-trace-phoenix |
| UI | http://localhost:6006 → 프로젝트 agent-trace. 포트는 127.0.0.1에만 바인딩되어 외부 접근 불가 |
| OTLP 수신 | http://127.0.0.1:6006/v1/traces (agent-trace 설정이 localhost 이외 호스트를 거부) |
| 데이터 | ~/.local/share/agent-trace/phoenix/ 볼륨. 컨테이너를 지워도 남는다 |
| 상태 확인 | docker ps --filter name=agent-trace-phoenix |
Langfuse(컨테이너 4개) 대신 Phoenix(컨테이너 1개, SQLite)를 고른 이유와 exporter를 표준 OTLP로만 유지해 백엔드를 교체 가능하게 둔 결정은 실행 가이드 참고.
agent-trace의 턴 하나 = trace 하나다. 루트가 AGENT span이고 그 아래 LLM 호출과 툴 호출이 자식 span으로 붙는다. 서브에이전트 턴은 부모 턴의 trace_id를 공유해 부모의 루트 span 아래에 중첩된다.
claude_code:ppi turn 12llm claude-opus-5tool Bashsubagent <이름>span 이름 규칙: 루트는 {도구}:{cwd 마지막 폴더명} turn {턴 번호}, 서브에이전트는 subagent {이름}, LLM은 llm {모델}, 툴은 tool {툴명}. 필터에서 쓸 수 있는 속성은 다음과 같다(2026-09-18 실제 export된 span에서 읽은 키).
| 속성 | 뜻 | 필터 예 |
|---|---|---|
agent.tool | claude_code / codex / hermes | agent.tool == 'codex' |
agent.cwd | 세션 작업 디렉터리 전체 경로 | 'ppi' in agent.cwd |
session.id | 세션 id. Sessions 탭 묶음 기준 | session.id == '…' |
input.value / output.value | 사용자 프롬프트 / 최종 응답 (8,000자 절단) | '다시' in input.value |
turn.index | 세션 내 턴 순번(0부터) | turn.index == 0 |
turn.latency_ms | 턴 지연. 도구가 보고한 값이 있으면 그 값 | turn.latency_ms > 300000 |
turn.after_compaction | 컨텍스트 컴팩션 직후 턴 | turn.after_compaction == True |
turn.aborted | 중단된 턴 (Codex만 감지됨) | turn.aborted == True |
turn.tool_error_count | 에러로 끝난 툴 호출 수 | turn.tool_error_count > 3 |
turn.thinking_tokens | 턴 내 thinking 토큰 합 | turn.thinking_tokens > 2000 |
agent.parent_turn_id | 서브에이전트 턴이면 부모 턴 id, 아니면 None | agent.parent_turn_id is None |
agent.subagent_name | 서브에이전트 이름(agentId 또는 nickname) | agent.subagent_name is not None |
| 종류 | 속성 |
|---|---|
| LLM | llm.model_name, llm.request_id, llm.stop_reason, llm.thinking_tokens, llm.token_count.prompt / .completion / .total / .prompt_details.cache_read, 그리고 같은 값의 gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens |
| TOOL | tool.name, input.value(입력 JSON), output.value(툴 출력, 8,000자 절단). 에러면 span status가 ERROR |
모든 span에 공통인 기본 필드: span_kind, latency_ms(span 자체 길이), status_code, name. 루트 span에서는 latency_ms와 turn.latency_ms가 거의 같지만, 후자는 도구가 보고한 turn_duration을 우선한다.
http://localhost:6006 → 프로젝트 agent-trace를 열면 상단에 세 탭이 있다.
| 탭 | 행 하나의 단위 | 언제 쓰나 |
|---|---|---|
| Traces | trace의 최상위 span = 최상위 턴 | 턴 목록 훑기. 행을 열면 span 트리(LLM·툴·서브에이전트)가 시간축으로 펼쳐진다 |
| Spans | 모든 span | 툴 호출·LLM 호출 단위로 찍어 볼 때. span_kind == 'AGENT'를 걸면 서브에이전트 턴까지 포함한 턴 목록이 된다 |
| Sessions | session.id로 묶인 세션 | 한 세션의 턴들을 시간순으로 이어 보며 어느 턴부터 꼬였는지 추적 |
서브에이전트 턴은 Traces 탭에 행으로 안 나온다. Phoenix OTel 계층에서는 부모 턴 아래 중첩된 자식이기 때문이다. 서브에이전트 턴만 모으려면 Spans 탭에서 span_kind == 'AGENT' and agent.subagent_name is not None.
행을 클릭하면 span 상세가 열린다. 왼쪽은 span 트리와 타임라인, 오른쪽은 선택한 span의 속성(Attributes)·입력/출력·Annotations 패널이다. 룰(rule:*)과 judge 주석은 루트 span에 붙어 있으므로 루트를 선택해야 보인다. 터미널 대응 명령은 uv run agent-trace turns --limit 20 [--flagged](목록)과 uv run agent-trace show <turn-id>(타임라인)이다. 긴 툴 출력은 UI가 훨씬 읽기 편하다.
Traces·Spans 탭 상단 필터칸에 Python 식처럼 쓴다. 아래는 2026-09-18 Phoenix 20.11.0 GraphQL 엔드포인트에 실제로 넣어 결과를 확인한 표현식이다.
== != > >= < <= and or in is None is not Noneagent.tool == 'codex'. ✗ attributes['agent.tool'] == 'codex'는 에러 없이 0건을 돌려준다.== True. ✗ == true는 0건.'문자열' in 속성. ✗ SQL식 like는 문법 에러.annotations['이름'].score / .label / .explanation. evals['이름']도 같은 뜻으로 동작한다.agent.tool == 'codex' # Codex 턴만
agent.tool == 'claude_code' and 'ppi' in agent.cwd # 특정 프로젝트의 Claude Code 턴
agent.cwd == '/Users/chulsu/git-projects/agent-trace' # 경로 정확히 일치
turn.latency_ms > 300000 # 5분 넘은 턴
turn.tool_error_count > 3 # 툴 에러 4회 이상
turn.after_compaction == True # 컴팩션 직후 턴
turn.aborted == True # 중단된 턴(Codex만)
turn.thinking_tokens > 2000 # thinking 많은 턴
'다시' in input.value # 프롬프트에 특정 단어
session.id == '<세션id>' # 한 세션의 턴만
agent.tool == 'codex' and latency_ms > 300000 and turn.tool_error_count > 0 # 조합
현재 이 프로젝트에 등록된 주석 이름은 8개다: rule:slow_turn rule:tool_loop rule:tool_error_ratio rule:user_correction rule:after_compaction rule:aborted judge manual_check.
annotations['rule:tool_loop'].score >= 3 # 동일 툴 호출 3회 이상 반복
annotations['rule:slow_turn'].score > 1 # 도구별 p95 임계 대비 배율 1 초과
annotations['rule:user_correction'].explanation is not None # 다음 프롬프트가 교정으로 시작한 턴
annotations['judge'].label != 'ok' # judge가 문제 있다고 본 턴
annotations['judge'].label == 'tool_misuse' # 특정 카테고리
annotations['judge'].score >= 2 # 심각도 2 이상
annotations['manual_check'].explanation is not None # 사람이 남긴 주석
| 주석 | annotator | score 의미 | label |
|---|---|---|---|
rule:slow_turn | CODE | 지연 ÷ 도구별 p95 임계값 (배율) | — |
rule:tool_loop | CODE | 동일 (툴, 입력) 반복 횟수 | — |
rule:tool_error_ratio | CODE | 에러 비율 (0.4 이상) | — |
rule:user_correction / after_compaction / aborted | CODE | 1.0 고정 | — |
judge | LLM | severity 0~3 | ok wrong_direction slow loop ignored_instruction over_scope tool_misuse |
manual_check(또는 사람이 정한 이름) | HUMAN | 자유 | 자유. sync-marks는 이름과 무관하게 HUMAN이면 전부 회수 |
span_kind == 'TOOL' and status_code == 'ERROR' # 실패한 툴 호출
span_kind == 'TOOL' and tool.name == 'Bash' and latency_ms > 60000 # 1분 넘은 Bash
span_kind == 'TOOL' and 'Error' in output.value # 출력에 Error 포함
span_kind == 'LLM' and llm.token_count.total > 100000 # 10만 토큰 넘는 단일 호출
span_kind == 'LLM' and llm.thinking_tokens > 5000
span_kind == 'AGENT' and agent.subagent_name is not None # 서브에이전트 턴만
agent.parent_turn_id is None and span_kind == 'AGENT' # 최상위 턴만
Traces에서 행을 열고 타임라인을 본다. 어느 툴 호출이 오래 걸렸는지, 에러가 어디서 났는지, 서브에이전트가 어느 툴 호출에서 갈라졌는지가 한 화면에 나온다. judge 리포트의 "+333316ms Chrome headless 스크린샷 218.9초" 같은 서술이 여기 타임라인 오프셋과 1:1로 대응한다.
4절 표현식을 조합한다. 현재 룰 분포(239건)는 slow_turn 138 · after_compaction 46 · user_correction 22 · aborted 16 · tool_loop 14 · tool_error_ratio 3이다. slow_turn은 툴 실행 시간이나 사용자 응답 대기를 에이전트의 느림과 구분하지 못해 오탐이 많으니, 신호가 명확한 tool_loop·user_correction·tool_error_ratio부터 보는 것이 효율적이다.
manual_check, label bad, 설명)uv run agent-trace sync-markshuman_marks로uv run agent-trace judge --limit 10 --pushjudge LLM 주석이 붙어 룰 주석과 나란히 보인다룰이 못 잡은 턴을 사람이 라벨하는 유일한 경로다. 2026-09-18 기준 사람 주석은 1건뿐이어서 거의 쓰이지 않았다. 주석 이름을 note로 남겨도 회수된다(Phoenix 클라이언트의 기본 note 제외를 agent-trace가 끈다).
uv run agent-trace propose --min-severity 2 # 제안을 (카테고리, 도구, cwd)로 묶어 표 + proposals/날짜.md
uv run agent-trace apply <proposal-id> # 대상 CLAUDE.md / AGENTS.md diff 미리보기
uv run agent-trace apply <proposal-id> --yes # "## agent-trace 개선 규칙" 절에 한 줄 append
uv run agent-trace resume <turn-id> # 그 세션을 이어갈 claude --resume / codex resume 명령 출력
Phoenix에서 이 단계는 안 보인다. Phoenix는 판정까지의 화면이고, 환류는 터미널과 지침 파일에서 일어난다.
| 기능 | Anthropic 구독(Claude Pro/Max) | API 키 |
|---|---|---|
agent-trace judge | 가능 — cli 백엔드가 로컬 claude CLI를 서브프로세스로 실행. Claude Code 자체를 돌리는 것이라 허용된 사용 | 가능 — sdk 백엔드, 호출마다 과금 |
| Phoenix Chat / Playground | 불가 — Phoenix 서버가 provider API를 직접 호출하는 구조. Claude Code 로그인 토큰을 꺼내 넣는 방식은 이용약관 위반이라 권하지 않음 | 가능 — UI 설정 또는 서버 env ANTHROPIC_API_KEY |
구독 경로를 고정하려면 ~/.config/agent-trace/.env에 AGENT_TRACE_JUDGE_BACKEND=cli를 둔다(2026-09-18 적용). 기본값 auto는 셸에 API 키가 잡히면 조용히 sdk로 넘어가 과금될 수 있다. 실측 판정 1건 약 31초, 타임아웃 180초. 큰 배치 전 --dry-run으로 프롬프트를 보고 --limit을 작게 시작한다.
backfill/watch를 켜면 OTLP exporter가 턴마다 10초 재시도 데드라인까지 기다려 몇 시간씩 걸릴 수 있다. agent-trace는 시작 시 preflight로 엔드포인트를 2초 확인해 바로 종료하며, 수집만 하려면 --no-export.backfill --force는 exported_at이 남아 있는 턴을 다시 보내지 않고(ended_at이 바뀐 턴만 재export), flag --push·judge --push도 pushed_at이 없는 것만 올린다. 그래서 초기화 뒤에는 SQLite의 표식을 먼저 지워야 한다. 드물게만 할 일이다.
docker compose -f deploy/phoenix/docker-compose.yml down
rm -rf ~/.local/share/agent-trace/phoenix
docker compose -f deploy/phoenix/docker-compose.yml up -d
sqlite3 ~/.local/share/agent-trace/agent-trace.db \
"UPDATE turns SET exported_at=NULL; UPDATE flags SET pushed_at=NULL; UPDATE verdicts SET pushed_at=NULL;"
uv run agent-trace backfill # 전 턴 재export
uv run agent-trace flag --push # 룰 주석 복원
uv run agent-trace judge --limit 0 --push # 기존 판정만 재push (새 판정 없이)sync-marks는 루트 span 100,000개·주석 1,000개까지 한 번에 가져온다. 주석이 그 이상 쌓이면 페이지네이션이 없어 누락된다(이월 항목).UI가 쓰는 것과 같은 GraphQL 엔드포인트에 filterCondition을 넣어 결과 유무와 에러를 확인했다. 새 표현식이 궤도에 맞는지 볼 때 같은 방법을 쓰면 된다.
PROJ=$(curl -s http://127.0.0.1:6006/graphql -H 'content-type: application/json' \
-d '{"query":"{ projects { edges { node { id name } } } }"}' \
| python3 -c "import sys,json; [print(e['node']['id']) for e in json.load(sys.stdin)['data']['projects']['edges'] if e['node']['name']=='agent-trace']")
curl -s http://127.0.0.1:6006/graphql -H 'content-type: application/json' -d @- <<JSON
{"query":"query(\$id: ID!, \$f: String){ node(id:\$id){ ... on Project { spans(first:1, filterCondition:\$f){ edges { node { name } } } } } }",
"variables":{"id":"$PROJ","f":"agent.tool == 'codex' and turn.latency_ms > 300000"}}
JSON
errors가 오면 문법 오류, edges가 빈 배열이면 문법은 맞지만 0건이다. attributes['…']가 후자에 해당해서 조용히 틀리는 함정이 된다.