Skip to content

feat(prompt): decouple prompt-facing choice ids from canonical catego… - #8

Merged
xianyu9n merged 3 commits into
masterfrom
feat/prompt-choice-identity
Aug 19, 2026
Merged

feat(prompt): decouple prompt-facing choice ids from canonical catego…#8
xianyu9n merged 3 commits into
masterfrom
feat/prompt-choice-identity

Conversation

@xianyu9n

@xianyu9n xianyu9n commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Overview

Decouple the canonical category_id from the model-visible prompt/action identity.

Canonical category_id (e.g. finance:业务.账户信息.基本信息) is needed for registry / corpus lookup, canonical ground truth, evaluation/reward and data joins — but it should not appear in prompts or be generated by the LLM. This PR introduces a prompt-facing choice protocol while keeping the canonical contract completely unchanged.

Design

old:
  canonical category_id ——(渲染进 prompt / 模型直接输出 canonical id)——> LLM

new:
  canonical category_id
    → PromptChoiceRegistry
    → choice_id ("1".."N",按 LeafRegistry.categories 稳定顺序)
    → Stage1 prompt: [["1","个人联系信息"], ...];模型输出 {"candidates":["3","7","12","19","24"]}
    → Stage2 prompt: 5 候选局部编号 "1".."5";模型输出 {"answer":"2"}
    → decode(Stage1 全局 choice_id / Stage2 位置编号)→ canonical category_id
    → 既有 evaluate_stage1/2 正确性逻辑(不变)

Boundary:

  • LLM boundary: choice_id, internal boundary: category_id
  • Choice ids never leak into corpus lookup / canonical target / reward semantics.

Changes

New: src/agent/task/prompt_choices.py

  • PromptChoice (choice_id / category_id / display_name)
  • PromptChoiceRegistry: bidirectional mapping, full registry coverage, unique choice ids, deterministic ("1".."N" by registry order), encode/decode_candidates
  • display_name = shortest unique path suffix: leaf name when unique, otherwise parent-qualified until unique (e.g. 基本信息账户信息 / 基本信息, 合约协议 / 基本信息, 营销服务 / 基本信息)
  • encode/decode_stage2_answer: positional local ids 1..5
  • PromptChoiceError for strict failures — no fuzzy/name fallback

Prompts: src/agent/task/prompts.py

  • Stage 1 catalog → compact [choice_id, display_name] pairs; system contract requires 5 unique choice ids, forbids canonical ids
  • Stage 2 bundle → local "id":"1".."5" + display_name + corpus description/examples
  • stage1_answer / stage2_answer emit choice ids

Evaluation: src/agent/evaluation/classification.py

  • evaluate_stage1_choices / evaluate_stage2_choices decode choice ids before delegating to the unchanged canonical evaluate_stage1/2 (which consume the unified parser check_stage1/2_output, shared with the RL reward adapter). No check logic duplicated; reward/evaluation stay canonical.

SFT: src/agent/training/sft/dataset.py

  • ground_truth / candidates columns keep canonical category_id; only messages speak choice ids
  • Validator decodes via the choice adapters

RL: src/agent/training/rl/dataset.py

  • RL exporter reuses the same prompt builders, so RL prompts now speak choice ids too; reward_model.ground_truth / extra_info.candidates stay canonical
  • RL validator's stage-1 catalog-coverage check adapted to [choice_id, display_name] format; canonical ids must not leak into prompts
  • No RL reward integration (deferred)

Examples

Stage 1 (finance)

// before
{"category_id":"finance:业务.账户信息.基本信息","name":"基本信息"}   // {category_id,name} 233 项
{"candidates":["finance:业务.账户信息.基本信息", "…5 个完整 canonical id…"]}
// after
[["1","个人基本概况信息"],["49","账户信息 / 基本信息"], ]
{"candidates":["49","54","18","40","135"]}

Stage 2

// before: {"category_id":"finance:业务.账户信息.基本信息","name":"基本信息",…},输出 {"answer":"finance:业务.账户信息.基本信息"}
// after
{"id":"1","name":"账户信息 / 基本信息","description":"","examples":[]},
{"id":"2","name":"合约协议 / 基本信息",}
{"answer":"2"}  →(decode)→  finance:业务.合约协议.基本信息

Invariants

  • canonical category_id unchanged
  • registry universe unchanged (full coverage enforced)
  • ground-truth semantics unchanged (target.category_id is the only label)
  • reward/evaluation final comparison still uses canonical IDs
  • training.common helpers reused (no duplication of canonical_target / build_candidates / corpus checks)
  • shougang/infra code strategy intact; finance duplicate leaf names disambiguated by display_name

Tests

  • python -m pytest -q181 passed, 2 skipped
  • skipped tests: verl module not installed locally (environment, unrelated)

Deferred

  • SFT/RL parquet re-export
  • prompt token stats recompute + prompt_length_analysis.md
  • artifacts/docs cleanup
  • RL reward choice-id decoding
  • formal SFT / VeRL RL smoke

曾立宏 added 3 commits August 19, 2026 13:32
…ry_id

Prompt-facing identity is now a compact choice protocol, never the
canonical category_id:

- New PromptChoiceRegistry: deterministic choice ids "1".."N" following
  LeafRegistry.categories order, plus shortest unique path suffix display
  names (duplicate leaf names are parent-qualified until unique). Strict
  PromptChoiceError on unknown ids / wrong counts / ambiguous names; no
  fuzzy fallback.
- Stage 1 catalog renders [choice_id, display_name] pairs; the model
  answers with global choice ids. Stage 2 renders the 5 candidates with
  local bundle ids "1".."5"; the model answers with the local id.
- evaluate_stage1_choices / evaluate_stage2_choices decode choice ids
  back to canonical category_id BEFORE delegating to the existing
  canonical evaluate_stage1/2 (which consume the unified parser's
  check_stage1/2_output shared with the RL reward adapter). No check
  logic is duplicated; reward/evaluation semantics stay canonical.
- SFT exporter/validator keep ground_truth and candidates columns as
  canonical category_id; only messages speak choice ids. The RL exporter
  reuses the same prompt builders, so RL prompts now speak choice ids too
  while reward_model.ground_truth and extra_info.candidates stay
  canonical; the RL validator's stage-1 catalog-coverage check was
  adapted to the [choice_id, display_name] format (canonical ids never
  leak into prompts).

canonical category_id, registry universe and ground-truth semantics are
unchanged (no data/artifacts/corpus modifications, no token stats
recomputation, no retraining, no RL reward integration). SFT/RL parquet
re-export and prompt token stats are deferred to the next stage.
…racts

The Stage 1 contract showcased {"candidates":["1","2","3","4","5"]} and the
Stage 2 contract showcased {"answer":"1"} as example shapes. These concrete
legal answers create position bias in the model. Replace them with
position-agnostic descriptions:

- Stage 1: "Return exactly one JSON object with key 'candidates'. The value
  must contain exactly five unique choice ids from the catalog."
- Stage 2: "Return exactly one JSON object with key 'answer'. Its value must
  be one of the five candidate ids '1' through '5'."

Tests updated to assert the new wording and to guard against future
reintroduction of real candidate ids in the system contracts.
With local Stage 2 ids, the old GT-first fixture made every Stage 2 gold
{"answer":"1"}. Keep the baseline no-hard-negative policy but permute the
bundle deterministically from the stable source_id:

- build_candidates(ground_truth, registry, *, source_id) in training.common
  (shared by SFT and RL): GT + first four registry negatives, then a
  deterministic Fisher-Yates shuffle keyed by a sha256 digest of source_id.
  No runtime randomness (hash() is per-process randomized and never used).
- Same source_id always yields the same bundle (reproducible across runs and
  exporters); GT always present, exactly 5 unique, GT not fixed at position 1.
- Stage 1 and Stage 2 of a sample share the same permuted bundle; the SFT
  validator re-derives it from the row source_id.

Tests: same-id determinism, id-varied orderings, GT membership, 5-unique,
GT position spread (all five positions appear over a synthetic id set), and
the SFT Stage 2 assistant answers are no longer all {"answer":"1"}.
@xianyu9n
xianyu9n merged commit 23e2000 into master Aug 19, 2026
3 checks passed
@xianyu9n
xianyu9n deleted the feat/prompt-choice-identity branch August 19, 2026 19:51
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