Skip to content

planner prompt를 고정 부분 앞, 가변 식별자 뒤 순서로 직렬화 - #58

Merged
Createyouracccount merged 4 commits into
mainfrom
feat/planner-prompt-prefix-order-20260905
Sep 7, 2026
Merged

planner prompt를 고정 부분 앞, 가변 식별자 뒤 순서로 직렬화#58
Createyouracccount merged 4 commits into
mainfrom
feat/planner-prompt-prefix-order-20260905

Conversation

@Createyouracccount

Copy link
Copy Markdown
Member

배경

PR #57 뒤 Qwen3.8 27B(Q4_K_M, Ollama)로 쓰기 Run이 완료되지만 planner 호출당 68/96/107초가 걸렸습니다.
호출 하나의 절반은 planning context prefill이고, user message 9,064바이트 중 capability catalog가
8,304바이트(92%)로 사실상 상수인데도 매 호출 다시 prefill되고 있었습니다. 원인은 wire 키 순서입니다.
callId·requestDigest·runId·authority·journalHeadDigest처럼 호출마다 바뀌는 값이 envelope과
context의 맨 앞에 있어, 앞에서부터 같은 token만 재사용하는 provider prefix cache가 첫 50토큰에서
끊깁니다.

XGENy 코드 없이 같은 요청에 새 식별자만 바꿔 두 번씩 보낸 실측입니다.

변형 1회 2회
현재 키 순서 72.2s 61.7s
가변 키를 뒤로 64.8s 30.7s
동일 요청 재전송 28.1s

이 이득은 model이 아니라 provider의 캐시 구조에서 오므로 Ollama·vLLM·llama.cpp에 같은 방식으로
작용하고 model 선택과 무관합니다.

변경 사항 (ADR-0036)

  • Wire 순서를 "고정 → 누적 → 가변"으로 고정합니다. Envelope: profileVersion, planningContext, callId, requestDigest. Context: profileVersion, capabilities, omittedCapabilities, catalogDigest, goal, planningConstraints, steps, omittedSteps, toolOutputs, totalSteps, verifiedCompletedSteps, runId, authority, authorityEpoch, journalSequence, journalHeadDigest. steps/toolOutputs는 턴마다 뒤에 추가되므로 같은 Run의 다음 턴에서도 이전 prefix가 재사용됩니다.
  • 키 순서는 이제 계약이므로 REQUEST_ENVELOPE_PROFILExgeny.planner-request/v2로 올립니다. Planning context profile은 v3을 유지합니다(내용과 canonical digest 불변).
  • Catalog 내용은 바꾸지 않습니다. outputSchema를 빼면 prompt가 29% 줄지만(3,683→2,621 token) model이 "read-text가 digest를 돌려준다"를 그 schema에서 배우므로 품질 검증 없이는 하지 않습니다.

데이터 경계

context_digestrequest_digest는 RFC 8785(JCS)로 계산되어 키 순서와 무관합니다. 변경 전 main에서
채취한 golden context_digest가 변경 후에도 같음을 테스트로 고정했습니다. Journal·Receipt·manifest·
protocol fixture는 바뀌지 않습니다. 바뀌는 것은 envelope revision이 들어가는 request_profile_digest
하나이며, 그 golden은 의도적으로 재채취했습니다. 이 PR 이전에 시작해 완료되지 않은 Run은 resume 시
configuration_mismatch로 닫힙니다
(ADR-0035 §4와 같은 결과, ADR-0036 §2에 명시).

실제로 동작하게 된 것

같은 machine, 같은 goal, 같은 GGUF, 같은 프로필(300s/1024)입니다.

BEFORE (PR #57)  27B c. write natural  COMPLETED 271s   호출별 68 / 96 / 107 s
AFTER  (v2 순서) 27B c. write natural  COMPLETED 189s   호출별 59 / 67 / 64 s   (wire 순서 확인 3/3)
                 config.toml workers 4→8
                 8B read COMPLETED 35s ; 27B probe PASS

2·3번째 호출이 각각 약 30초·43초 줄었습니다. 1번째 호출은 catalog가 아직 캐시에 없어 그대로입니다.
2번째 호출은 tool output이 더해져 prompt가 3,144→3,627 token으로 늘었는데도 빨라졌습니다.

검증

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked --no-fail-fast (566 passed / 0 failed, 45 binaries)
  • cargo build --workspace --release --locked
  • xgeny protocol check
  • sh scripts/check-rc3-public-docs.sh
  • sh scripts/check-third-party-licenses.sh --check
  • 실제 로컬 endpoint(Ollama 27B·8B)에서 위 before/after와 wire 순서 확인

실패 테스트를 먼저 추가해 red를 확인한 뒤 구현했습니다: runtime은 직렬화된 context의 키 위치와
순서 무관 golden digest, provider는 envelope v2와 user message 바이트 순서를 검사합니다.

범위 밖

Catalog 축소(outputSchema·$schema), proposal 스키마 maxLength/pattern, --allow-file scope
제약은 별도입니다. 이 PR은 게시나 태그 생성을 수행하지 않습니다.

Planner user message는 envelope과 planning context를 serde 필드 순서대로 직렬화하는데,
callId·requestDigest·runId·authority·journalHeadDigest처럼 호출마다 바뀌는 값이 맨 앞에
있고 9,064바이트 중 92%를 차지하는 capability catalog가 맨 뒤에 있었다. Provider의
prefix cache는 앞에서부터 같은 token만 재사용하므로 매 호출 catalog prefill을 반복했다.
Qwen3.8 27B(Ollama)에서 같은 요청에 새 식별자만 바꿔 두 번 보내면 현재 순서는
72.2초/61.7초, 가변 키를 뒤로 보낸 순서는 64.8초/30.7초다.

ADR-0036에 따라 wire 순서를 "고정(catalog, goal) → 누적(steps, toolOutputs) → 가변
식별자"로 바꾸고 REQUEST_ENVELOPE_PROFILE을 v2로 올린다. context_digest와
request_digest는 RFC 8785 정규화라 순서와 무관하며 golden 테스트로 불변을 고정했다.
Envelope revision은 request profile digest 입력이므로 진행 중이던 Run의 resume은
configuration_mismatch가 되며 ADR에 명시했다. Catalog 내용은 바꾸지 않는다.

실측: 27B 쓰기 Run 호출별 68/96/107초 → 59/67/64초, 총 271초 → 189초. 8B 회귀 없음.
Planner에 보내는 strict JSON Schema는 Core가 독립 검증하는 상한(maxLength 5000 등)을 그대로
담고 있었다. llama.cpp는 이 상한을 GBNF로 컴파일하지 못해 HTTP 200으로 제약 없는 출력을
반환했고, PR #56의 production 스키마 프로브가 이를 chat_completions_incompatible로 닫았다.
Ollama는 상한 유무에 영향이 없다(8B 3회씩 A/B 동일). 스키마 pattern은 Ollama가 어떤 형태든
400으로 거부해 이식 가능하지 않으므로 넣지 않는다.

ADR-0037에 따라 maxLength/maxItems를 제거해 스키마는 구조만 강제하고(revision v2), 두 system
prompt 끝에 한 줄 minified JSON 문장과 Core step key 규칙 문장을 더한다(template v4-compact).
8B no-think model은 쓰기 카탈로그 prompt에서 1024 token 안에 100% 잘렸는데 compact 문장으로
100% 순응했고, key 문장은 출력을 767~854에서 577 token으로 줄이며 key를 유효하게 유지했다.
27B는 이미 compact라 변화가 없다. Request profile golden은 의도적으로 재채취했으며 진행 중이던
Run의 resume은 configuration_mismatch가 된다.

실측: brew llama.cpp 8B와 Ollama 번들 llama.cpp 27B 모두 model setup PASS(이전 incompatible),
27B 읽기 Run COMPLETED, Ollama 27B 쓰기 Run 162초로 회귀 없음, Ollama 8B 쓰기 Run 2/2 COMPLETED.
…0260906

모델용 proposal 스키마에서 상한을 빼고 system prompt가 compact 출력과 key 규칙을 말하도록 변경
@Createyouracccount
Createyouracccount merged commit d1c031f into main Sep 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant