0Pricing
Claude Architect · 课时

自我纠正

提取 calculated_total 和 stated_total,以发现数值偏差。

自我纠正 是 CoddyKit 上的免费 Claude Architect 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Claude Architect 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Claude Architect 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

The Trust Gap

When Claude extracts data from an invoice or report, the output can be perfectly valid JSON and still be wrong. A line item might be misread, a number transposed, or a subtotal silently drifted.

Schema validation catches structural problems: missing required fields, wrong types, bad enums. It does not catch a number that is well-formed but inconsistent with the rest of the document.

This lesson teaches self-correction: a technique where you extract enough information to let the system check its own arithmetic and catch drift before it reaches your database.

What 'Drift' Looks Like

Consider an invoice that lists line items and prints a total at the bottom. Two numbers are in play:

  • stated_total — the total literally printed on the document
  • calculated_total — the sum of the individual line items

On a clean document they match. But OCR noise, a misread digit, or a hallucinated line item makes them diverge. That divergence is drift.

The core idea of this lesson: if you only capture one of these two numbers, you can never detect the disagreement. Capture both, and the discrepancy becomes visible and machine-checkable.

Extract Both, Not One

The self-correction pattern starts in the schema. Instead of asking for a single total, you ask Claude to surface the raw materials for a consistency check.

Force structured output with a tool plus a JSON Schema so you eliminate syntax errors and guarantee the fields are present and typed. Use tool_choice set to any (or a forced tool) so the model must return the tool call rather than free-form prose.

import anthropic

client = anthropic.Anthropic()

extract_tool = {
    "name": "extract_invoice",
    "description": "Extract invoice line items plus BOTH the summed line-item total and the total printed on the document, so a downstream check can detect drift.",
    "input_schema": {
        "type": "object",
        "properties": {
            "line_items": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "description": {"type": "string"},
                        "amount": {"type": "number"}
                    },
                    "required": ["description", "amount"]
                }
            },
            "calculated_total": {"type": "number"},
            "stated_total": {"type": "number"}
        },
        "required": ["line_items", "calculated_total", "stated_total"]
    }
}

resp = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=[extract_tool],
    tool_choice={"type": "any"},
    messages=[{"role": "user", "content": invoice_text}],
)

Required Means Always Present

One subtle rule: mark a field required only if it is always present in the source. Both calculated_total and stated_total qualify here — every invoice has line items to sum and a printed total to read.

But never require a field that may be absent. If you force a field that isn't in the document, the model will fabricate a plausible value to satisfy the schema — and now you've manufactured the very drift you were trying to catch.

For optional structures, leave the field out of required and let it be omitted. For extensible categoricals, use an enum with an "other" value plus a free-text detail field.

The Validation Step

Once both numbers are in hand, validation is plain code — deterministic, fast, and free. Use a Pydantic-style validator so the check lives next to the schema and runs on every extraction.

The model decides what the numbers are; your code decides whether they agree. Keep the consistency rule in hard code, not in a prompt, because arithmetic agreement is exactly the kind of guarantee that deterministic logic provides and probabilistic prompting cannot.

from pydantic import BaseModel, model_validator

class Invoice(BaseModel):
    line_items: list[dict]
    calculated_total: float
    stated_total: float

    @model_validator(mode="after")
    def check_drift(self):
        summed = round(sum(li["amount"] for li in self.line_items), 2)
        if abs(summed - self.calculated_total) > 0.01:
            raise ValueError(
                f"calculated_total {self.calculated_total} "
                f"does not match line-item sum {summed}"
            )
        if abs(self.calculated_total - self.stated_total) > 0.01:
            raise ValueError(
                f"DRIFT: calculated_total {self.calculated_total} "
                f"!= stated_total {self.stated_total}"
            )
        return self

Retry With Feedback

When the validator raises, don't just discard the output and re-run blind. Use retry-with-feedback: send the model the original document, its own wrong output, and the exact validation error.

This works precisely because the failure is an arithmetic / structural error — the information needed to fix it is present in the source; the model just has to recompute. Giving it the specific error turns a guess into a targeted correction.

def extract_with_retry(invoice_text, max_attempts=3):
    messages = [{"role": "user", "content": invoice_text}]
    for _ in range(max_attempts):
        resp = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            tools=[extract_tool],
            tool_choice={"type": "any"},
            messages=messages,
        )
        data = resp.content[0].input
        try:
            return Invoice(**data)
        except ValueError as err:
            messages.append({"role": "assistant", "content": resp.content})
            messages.append({"role": "user", "content":
                f"That extraction failed validation: {err}. "
                f"Re-read the document and correct the numbers."})
    raise RuntimeError("extraction did not converge")

When Retry Won't Help

Retry-with-feedback is powerful but it is not magic. It fixes format, structural, and arithmetic errors — cases where the right answer is recoverable from the source.

It does not help when information is simply absent from the document. If an invoice has no stated total at all, looping the model just pressures it to invent one. Re-reading the same blank space cannot produce a fact that was never there.

So branch your logic: discrepancy between two present numbers → retry. A genuinely missing field → mark it absent (leave it out of required) and escalate or flag, never fabricate.

# stated_total is OPTIONAL in the schema, not required
class Invoice(BaseModel):
    line_items: list[dict]
    calculated_total: float
    stated_total: float | None = None  # may be absent on the document

    @model_validator(mode="after")
    def check_drift(self):
        if self.stated_total is None:
            # Don't retry an absent fact -- flag for human review
            self.flags = ["stated_total_missing"]
            return self
        if abs(self.calculated_total - self.stated_total) > 0.01:
            raise ValueError("DRIFT detected")  # recoverable -> retry
        return self

Cap Iterations As A Safety Net

Notice max_attempts in the retry loop. That cap is a safety net, not the primary control flow. The loop's real exit is success: the validator passes and you return.

This mirrors the agentic loop everywhere in Claude architecture — you terminate on a concrete condition (here, validation passing), and you keep a hard cap only to prevent a pathological document from looping forever. Never let an arbitrary iteration count become your stop logic, and never parse the model's prose for words like "corrected" to decide you're done.

Independent Review Beats Self-Review

Deterministic arithmetic checks catch numeric drift. But for judgment-level correctness — "did this extraction misclassify the vendor?" — a model reviewing its own same-session output is weak. The author retains its original reasoning and tends not to challenge itself.

When you need a semantic second opinion, run an independent, fresh-instance review: a separate request (or subagent) that sees only the document and the extracted output, not the original chain of thought. Fresh eyes catch what self-review rationalizes away.

review = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=512,
    system=(
        "You are an independent auditor. You did NOT produce this "
        "extraction. Compare the JSON against the source document and "
        "report any field that contradicts the source."
    ),
    messages=[{"role": "user", "content":
        f"SOURCE:\n{invoice_text}\n\nEXTRACTION:\n{extracted_json}"}],
)

Keep Case Facts Verbatim

Self-correction depends on numbers staying exact through your pipeline. The danger is progressive summarization: as context is compacted, totals, percentages, and dates go vague — "around 4,000" instead of 3,981.50.

Pull the transactional facts — calculated_total, stated_total, line amounts — into a separate "case facts" block kept verbatim, outside any summary. Trim verbose tool output to only the fields you need, but never let a summarizer touch the numbers your validator must compare.

Track Provenance For Drift

When the two totals disagree, you want to know why, fast. Keep provenance: map each extracted number back to its source — the line of text it came from, and where on the document.

With provenance, a drift alert becomes actionable: "calculated_total summed five line items, but the document shows six." Without it, you only know the numbers disagree, not which one to trust. And render results by content type — financial figures belong in a table where mismatches are easy to scan, not buried in prose.

# Annotate each figure with where it came from
"line_items": [
    {"description": "Consulting", "amount": 1200.00,
     "source": {"page": 1, "text": "Consulting ... 1,200.00"}}
],
"stated_total": {"value": 3981.50,
                 "source": {"page": 1, "text": "TOTAL DUE 3,981.50"}}
# On drift, the alert can name the exact conflicting source lines.

Quick Check: Designing The Drift Catch

You are extracting invoice totals and must reliably catch cases where the printed total disagrees with the line items. Which design best implements self-correction?

Recap: Catching Drift

Key takeaways for self-correction in extraction pipelines:

  • Extract both numbers. Capture calculated_total and stated_total — you can't detect drift from a single value.
  • Validate in code. Use Pydantic-style deterministic checks; keep arithmetic guarantees out of prompts.
  • Retry with feedback (original doc + wrong output + exact error) fixes format/structural/arithmetic errors — but NOT genuinely absent data.
  • Required = always present. Never require a maybe-absent field, or the model fabricates it.
  • Iteration caps are a safety net; the real exit is validation passing — never parse prose for completion.
  • Independent fresh-instance review beats same-session self-review for semantic correctness.
  • Protect the numbers: keep case facts verbatim outside summaries, and track provenance so a drift alert tells you which value to trust.

常见问题解答

「自我纠正」课时是免费的吗?

是的 — 「自我纠正」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Claude Architect 课程的其余内容,请升级到 CoddyKit PRO。 Claude Architect 课程共包含 4 节课。

「自我纠正」这节课中我会学到什么?

提取 calculated_total 和 stated_total,以发现数值偏差。 你通过在浏览器中直接运行的动手代码来练习 Claude Architect,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Claude Architect 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Claude Architect 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「自我纠正」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Claude Architect 课中编写并运行代码吗?

能。每节 Claude Architect 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 何时重试有帮助(以及何时无效)
  2. 带反馈的重试提示词
  3. 自我纠正
  4. 多轮处理与独立审查
← 返回 Claude Architect