Phoenix 트레이스 대시보드 사용법
— agent-trace 턴 탐색·필터·주석 활용

마지막 업데이트 2026-09-18

agent-trace가 로컬 Phoenix로 보낸 Claude Code · Codex · Hermes 턴 trace를 UI에서 찾고, 읽고, 주석해서 judge와 개선 루프로 넘기는 방법 (2026-09-18, Phoenix 20.11.0 실서버 검증 기준)

한 줄 요약. 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-marksjudge --push를 돌린다.

1. Phoenix가 무엇이고 어디에 떠 있나

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
UIhttp://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로만 유지해 백엔드를 교체 가능하게 둔 결정은 실행 가이드 참고.

2. 무엇이 어떻게 들어가 있나 — trace 구조와 span 속성

agent-trace의 턴 하나 = trace 하나다. 루트가 AGENT span이고 그 아래 LLM 호출과 툴 호출이 자식 span으로 붙는다. 서브에이전트 턴은 부모 턴의 trace_id를 공유해 부모의 루트 span 아래에 중첩된다.

AGENT (루트)claude_code:ppi turn 12
프롬프트·최종응답·지연·플래그
LLM (자식)llm claude-opus-5
모델·토큰·stop_reason
TOOL (자식)tool Bash
툴 이름·입력·출력·에러
AGENT (손자)subagent <이름>
Agent 툴이 띄운 서브에이전트 턴

span 이름 규칙: 루트는 {도구}:{cwd 마지막 폴더명} turn {턴 번호}, 서브에이전트는 subagent {이름}, LLM은 llm {모델}, 툴은 tool {툴명}. 필터에서 쓸 수 있는 속성은 다음과 같다(2026-09-18 실제 export된 span에서 읽은 키).

AGENT span (턴)

속성필터 예
agent.toolclaude_code / codex / hermesagent.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, 아니면 Noneagent.parent_turn_id is None
agent.subagent_name서브에이전트 이름(agentId 또는 nickname)agent.subagent_name is not None

LLM span / TOOL span

종류속성
LLMllm.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
TOOLtool.name, input.value(입력 JSON), output.value(툴 출력, 8,000자 절단). 에러면 span status가 ERROR

모든 span에 공통인 기본 필드: span_kind, latency_ms(span 자체 길이), status_code, name. 루트 span에서는 latency_msturn.latency_ms가 거의 같지만, 후자는 도구가 보고한 turn_duration을 우선한다.

3. 화면 구성 — 어디서 무엇을 보나

http://localhost:6006 → 프로젝트 agent-trace를 열면 상단에 세 탭이 있다.

행 하나의 단위언제 쓰나
Tracestrace의 최상위 span = 최상위 턴턴 목록 훑기. 행을 열면 span 트리(LLM·툴·서브에이전트)가 시간축으로 펼쳐진다
Spans모든 span툴 호출·LLM 호출 단위로 찍어 볼 때. span_kind == 'AGENT'를 걸면 서브에이전트 턴까지 포함한 턴 목록이 된다
Sessionssession.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가 훨씬 읽기 편하다.

4. 필터식 문법 — 실서버에서 검증한 것만

Traces·Spans 탭 상단 필터칸에 Python 식처럼 쓴다. 아래는 2026-09-18 Phoenix 20.11.0 GraphQL 엔드포인트에 실제로 넣어 결과를 확인한 표현식이다.

규칙 5가지

  1. 연산자: == != > >= < <= and or in is None is not None
  2. 속성은 점 이름 그대로: agent.tool == 'codex'. attributes['agent.tool'] == 'codex'는 에러 없이 0건을 돌려준다.
  3. 불리언은 대문자: == True. == true는 0건.
  4. 문자열은 홑따옴표, 부분 일치는 '문자열' in 속성. SQL식 like는 문법 에러.
  5. 주석은 annotations['이름'].score / .label / .explanation. evals['이름']도 같은 뜻으로 동작한다.

턴 찾기 (Traces 탭)

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   # 조합

룰·judge·사람 주석으로 찾기

현재 이 프로젝트에 등록된 주석 이름은 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          # 사람이 남긴 주석
주석annotatorscore 의미label
rule:slow_turnCODE지연 ÷ 도구별 p95 임계값 (배율)
rule:tool_loopCODE동일 (툴, 입력) 반복 횟수
rule:tool_error_ratioCODE에러 비율 (0.4 이상)
rule:user_correction / after_compaction / abortedCODE1.0 고정
judgeLLMseverity 0~3ok wrong_direction slow loop ignored_instruction over_scope tool_misuse
manual_check(또는 사람이 정한 이름)HUMAN자유자유. sync-marks는 이름과 무관하게 HUMAN이면 전부 회수

툴·LLM 호출 단위로 찾기 (Spans 탭)

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'        # 최상위 턴만

5. 활용 흐름 네 가지

① 턴을 눈으로 뜯어보기

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부터 보는 것이 효율적이다.

③ 사람 주석으로 judge 대상 넓히기

1. UI에서 표시루트 span 선택 → Annotations → 추가 (annotator HUMAN, 이름 예 manual_check, label bad, 설명)
2. 회수uv run agent-trace sync-marks
HUMAN 주석만 human_marks
3. 판정uv run agent-trace judge --limit 10 --push
사람 표시 턴도 기본 대상에 포함
4. 확인같은 span에 judge 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는 판정까지의 화면이고, 환류는 터미널과 지침 파일에서 일어난다.

6. 구독 모델과 API 키의 경계

기능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/.envAGENT_TRACE_JUDGE_BACKEND=cli를 둔다(2026-09-18 적용). 기본값 auto는 셸에 API 키가 잡히면 조용히 sdk로 넘어가 과금될 수 있다. 실측 판정 1건 약 31초, 타임아웃 180초. 큰 배치 전 --dry-run으로 프롬프트를 보고 --limit을 작게 시작한다.

7. 데이터 유지보수

8. 검증 방법 — 이 문서의 필터식은 어떻게 확인했나

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['…']가 후자에 해당해서 조용히 틀리는 함정이 된다.

관련 문서