Skip to content

fix(llm): tell the model the field names, not just "match the schema" - #132

Merged
wu908 merged 1 commit into
mainfrom
fix/structured-output-schema-hint
Sep 18, 2026
Merged

wu908 merged 1 commit into
mainfrom
fix/structured-output-schema-hint

Conversation

@wu908

@wu908 wu908 commented Sep 18, 2026

Copy link
Copy Markdown
Owner

在线上验证「提炼候选」修复时发现的第二层问题:视图虽然 201 了,但 proposal_sourcedeterministic_fallback——AI 路径失败后静默降级。服务器日志:连续 4 次「Invalid structured LLM output; retrying」。

根因

generate_structured 只告诉模型「Respond only with valid JSON matching the requested schema」,但从没把 schema 发给模型——字段名、类型全靠猜。仓库里有 5 个提示词属于这种情况(观点、系列、机会、意图草案、发布前检查),而带显式 JSON 示例的 3 个正是前几轮修过的(#85 让消化期静默降级的就是同一个病)。当时是逐提示词补示例,没有在边界上修。

修法

generate_structured 现在把 pydantic 模型自身的字段骨架写进系统提示(字段名、类型、字面量取值、嵌套模型),例如:

{"statement": "", "rationale": "", "limitations": [""]}

骨架是从 model_fields 走出来的(带自引用深度保护),所以不可能与"校验用的契约"漂移;所有调用方一起受益,不用动任何提示词。消化期 / 起点 / 锚点提示词里手写的示例保留(它们描述语义),且现在与骨架一致。

测试与验证

新增测试:结构化调用必须把字段名写进系统提示(桩客户端捕获提示内容,同时断言调用方自己的 system prompt 还在)。后端全量 + ruff 待跑完确认;部署后再打一次同一个线上调用,期望 proposal_source 变成 ai(这是这次修复的判据)。

Verifying the viewpoint fix on the live server, the draft came back with
`proposal_source: deterministic_fallback` — i.e. the AI path had failed and the product
quietly degraded. The server log said 「Invalid structured LLM output; retrying」 four times.

Root cause: `generate_structured` instructs the model to "Respond only with valid JSON
matching the requested schema" and never sends the schema. The model has to guess field
names and types. Five prompts in the repo ask for structured output without writing the
shape out themselves (viewpoint, series, opportunity, intent drafts, publish check); the
three that do carry an explicit example are the ones fixed in earlier rounds. This is the
same failure that made the digest silently degrade before (#85) — the lesson was applied
per-prompt instead of at the boundary.

The instruction now carries a skeleton generated from the pydantic model itself: field
names, types, first literal value, and nested models, e.g.

    {"statement": "", "rationale": "", "limitations": [""]}

which every caller benefits from, without touching any prompt. The skeleton is built by
walking `model_fields` (with a depth guard for self-referential models), so it cannot drift
from the contract the response is validated against.

Test: `generate_structured` must put the field names in the system prompt — a stub client
captures the prompt and asserts both the schema fields and the caller's system prompt are
present. Hand-written examples in the digest / project-start / anchor prompts stay as they
are; they describe semantics, and they now agree with the skeleton.
@wu908
wu908 merged commit 57d1244 into main Sep 18, 2026
2 checks passed
@wu908
wu908 deleted the fix/structured-output-schema-hint branch September 18, 2026 14:59
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