Skip to content
vp.Ven Petrov
WorkAI demoWritingResume / CVAboutAt a glance
← Projects & notes
Finance evidence deskProject story · 8 min read

Does a reviewer make a finance agent more reliable?

16 of 24 planned single-analyst answers passed all exact checks. Adding a reviewer reduced that to 14 and roughly doubled observed elapsed time. The recorded failures show what changed.

Ven PetrovInteractive experimentOpen project

I had already turned a public financial report into an interactive explorer. I could compare periods and follow a number back to its page. The next question was whether an agent could choose the right evidence for a financial question, and whether adding a reviewer would improve the answer.

In this test, the single analyst did better. The separate reviewer added work and reduced the number of answers passing the checks. The failed outputs show where the added review changed a supported answer.

That sounds straightforward until you look at what can go wrong. Quarterly operating income increased by $86 million, while operating cash flow fell by $1,929 million. Both statements are correct. Explaining the cash movement as if it were the income movement would still produce a convincing paragraph with real numbers in it.

I used Codex and specialist agents to build this experiment. It uses public Novartis material and is independent demonstration work. It is not an internal company system or work I performed for Novartis.

Open the evidence desk. The calculated examples run online. The model comparison replays recorded calls so every visitor can inspect the same evidence.

Give the agent enough freedom to make a meaningful choice

I kept the existing dataset: 868 financial cells, including 115 explicitly unavailable values. I put it in a read-only SQLite database. Each fact retains its period, unit, accounting basis and source page.

The agent gets a catalog of available metrics, without their values. It can request figures, search eight reviewed report summaries using SQLite full-text search, or ask Python to calculate a comparison. It chooses which tools to use and which evidence to include in the answer. It cannot write arbitrary SQL, change the data or look elsewhere for a convenient explanation.

The search is deliberately small and lexical. It retrieves reviewed summaries, not every paragraph of the 54-page report. A failed search therefore tells us something about this small corpus, not whether the original report contains an answer somewhere else.

Keep the arithmetic out of the prose generator

The model selects references for numeric results. Python supplies the values from the observed facts or calculations. A missing prior-period cell stays missing. The comparison tool rejects a full-year amount paired with a quarterly amount. The model’s prose still needs its own review.

One useful example is free cash flow. Novartis defines it using operating cash flow less purchases of property, plant and equipment. Deducting intangible purchases as well would change that definition. The tool checks the arithmetic; the answer still has to explain which definition it used.

Another detail is the cash-flow table's combined lines. A movement labelled working capital and other operating cash-flow items should not be shortened into a claim about working capital alone. The number would survive that edit, but its meaning would change.

These checks do not make the prose automatically true. A valid citation can sit beside an unsupported cause. I kept exact value and reference checks separate from reviewing what the sentences actually say.

Give both review approaches the same starting point

I saved one initial analyst answer for each question. Two branches then received that exact draft and the evidence it had gathered. One branch had a separate reviewer role. The other asked the original analyst to take a second pass.

All three approaches use the same fixed model. Both review branches are fresh requests supplied with the saved draft and evidence. What changes is the role instruction, not the model's knowledge or access to another dataset.

Both branches received the same tools and additional budget: at most two model requests and six tool calls. The initial analyst had at most three requests. That extra control matters. If a reviewer improves an answer, I want to know whether the improvement comes from the different role or simply from another chance to inspect it.

If the first agent fails to produce a valid draft, both review branches are skipped and counted as unsuccessful answer opportunities. This version tests reviewing accepted drafts. It does not test whether another agent can rescue a malformed response.

The questions and expected results were frozen before the measured runs. They cover amounts, definitions, mixed arithmetic and explanation, unavailable figures, incompatible periods and an untrusted instruction embedded in a question. Gold numeric values and case-specific citation targets stay in the evaluator. The prompt does supply accounting guardrails, including period compatibility and the company’s free-cash-flow definition. This tests how the agent uses tools and evidence under those rules; it does not test whether it can discover the accounting rules itself.

This is a paired experiment on one report. The branches share a draft, so they are correlated observations. They are not three independent sets of financial work.

What the recorded runs showed

I ran twelve questions twice using nvidia/nemotron-3-super-120b-a12b:free, pinned to the Nvidia provider through OpenRouter. That produced 24 planned answers for each approach. The single analyst passed every exact reference, quantity and status check on 16 of 24. The analyst/reviewer pair passed 14 of 24. The analyst taking a second pass passed 12 of 24.

Measured local experiment

What changed with a reviewer?

Recorded results

Compare the selected figures and references, the time taken, and whether each explanation matched the report. These measurements answer different questions, so I keep them separate.

How the comparison was controlled

Prospective within-one-report pilot: reviewed normalized facts and eight short curated paraphrases, with lexical retrieval only; not full-report RAG or a broad unseen-document benchmark.

12 distinct held-out questions with records4 development / 12 held-out in the full set · 2 repeatsnvidia/nemotron-3-super-120b-a12b:free

Paired comparison: both review branches reuse the same first draft, with matched extra budgets. The reviewer role and self-review control are configured with the same fixed model. End-to-end totals include that draft once per branch. Do not sum the branch totals to estimate actual usage. These are correlated observations from one report.

4 complete record groups have unverified model or provider matching because one or more branches returned no response. Unknown conditions count as neither a match nor a mismatch.

Single analyst draft16 / 24
20 / 24 runs completed · 183 / 216 individual checks
Analyst + independent model reviewer14 / 24
19 / 24 runs completed · 175 / 216 individual checks
Analyst self-review control12 / 24
17 / 24 runs completed · 163 / 216 individual checks

An answer opportunity passes only when its automatic checks pass. Counts include repeated questions. These cover declared values, status and references, not overall answer quality. Missing planned runs stay in the answer denominator; individual check totals cover recorded runs only. Time and tool counts include the shared draft for each branch.

Separate from automatic checks

Did the answer mean the right thing?

Single analyst draftModel-assisted full rubric: 12 / 24 answer opportunities passedModel-assisted source review
Analyst + independent model reviewerModel-assisted full rubric: 9 / 24 answer opportunities passedModel-assisted source review
Analyst self-review controlModel-assisted full rubric: 9 / 24 answer opportunities passedModel-assisted source review

Each outcome covers the whole declared source rubric. A failed attempt with no answer is an unsuccessful answer opportunity. Model-assisted source review does not provide human financial assurance.

Held-out provider attempts140
Failed provider requests0
Failed runs16
Reported held-out inference cost$0

Held-out physical usage deduplicates shared call IDs across the strategy views. Sum of returned per-request costs; any missing usage makes actual cost unknown, not zero.

Some development requests returned no usage. Actual total cost across development and held-out runs is unknown; the held-out returned cost above remains separately reported. The raw evaluation includes both accounting scopes.

All measured counts, cost and limitations
End-to-end totals include the shared draft. Observed elapsed time includes local quota pacing and provider time; it is not isolated inference latency.
StrategyRunsAnswers passing checksIndividual checksCompletedEnd-to-end model callsExtra model callsEnd-to-end tool callsMedian elapsedReported cost
Single analyst draft2416 / 24183 / 21620 / 2465656434.39 s$0
Analyst + independent model reviewer2414 / 24175 / 21619 / 241033810269.11 s$0
Analyst self-review control2412 / 24163 / 21617 / 241023710071.44 s$0
  • Twelve held-out questions within one report, with two repeats per planned case; no significance, broad generalization or production-reliability claim.
  • Numeric values are hydrated by Python from observed references. Scores test evidence/metric/period selection and answer construction, not independent model arithmetic.
  • A valid citation ID does not establish a faithful paraphrase or causal explanation.
  • If returned models/providers differ between branches, report the confound rather than attribute the difference solely to review.
  • Automatic pass means the frozen exact reference/status/citation checks passed; it is not a semantic accuracy score.
  • Manual review remains pending unless an identified source reviewer supplies a complete rubric record; model-assisted review is explicitly labeled.
  • Shared drafts create paired, correlated observations. Reviewer versus self-review differences are only interpretable under equal added budgets and actual model/provider matching.
  • Missing planned records stay in automatic run denominators. Granular check totals cover recorded runs only.
  • Unnumbered legacy records receive saved-order replicate slots; experiment manifests should always stamp explicit replicate numbers.
  • The original model declaration is preserved. Any availability replacement is separately timestamped before heldout runs; models must stay fixed within heldout evaluation.
Paired comparison conditions

Matching the draft, question, added budget, model and provider matters when interpreting a reviewer effect.

[
  {
    "question_id": "heldout-01",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-01",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-02",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-02",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-03",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": null,
    "returned_providers_match": null
  },
  {
    "question_id": "heldout-03",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": null,
    "returned_providers_match": null
  },
  {
    "question_id": "heldout-04",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-04",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-05",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-05",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-06",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-06",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-07",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-07",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-08",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-08",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-09",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": null,
    "returned_providers_match": null
  },
  {
    "question_id": "heldout-09",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": null,
    "returned_providers_match": null
  },
  {
    "question_id": "heldout-10",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-10",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-11",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-11",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-12",
    "replicate": 1,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  },
  {
    "question_id": "heldout-12",
    "replicate": 2,
    "all_three_records_present": true,
    "shared_parent_matches": true,
    "same_question": true,
    "added_budgets_equal": true,
    "returned_models_match": true,
    "returned_providers_match": true
  }
]

The initial analyst made 65 model requests. The separate reviewer added 38; self-review added 37. Because both branches reuse the same draft, the experiment made 140 physical model requests, not the sum of three independent workflows. Every measured request returned a cost of $0.

The median observed duration was about 34 seconds for the first answer, 69 seconds including the reviewer and 71 seconds including self-review. These durations include local quota pacing and provider time. They are not an isolated inference-speed benchmark.

An independent coding agent also checked every headline, claim, result and limitation against the sources using the frozen rubric. That model-assisted source audit passed 12 of 24 single-analyst answers, 9 of 24 reviewer answers and 9 of 24 self-review answers. It is a separate review with inspectable notes, not human financial assurance or a universal accuracy score.

Before the measured run, four practice requests to a free Gemma endpoint were rate-limited. I changed to Nvidia before starting the fixed comparison and retained the practice records. Those four rejections returned no usage amount, so the total including development is recorded as unknown rather than inventing a zero. Every request was restricted to zero-priced routing, with no paid fallback.

What changed in two answers

In the second run of the SG&A question, Python calculated a signed change of +61 USD million from −3,440 − (−3,501). The initial headline said the expense "changed by +61 USD million". The reviewer changed that to "increased by +61 USD million". The published expense amounts became less negative, which means the expense was lower. The number and its references still passed the exact checks, but the new headline failed the separate source review.

In another second run, the analyst correctly compared full-year IFRS net income of 13,967 with 11,939 USD million, an increase of 2,028 USD million, using the report's income statement. The reviewer received those same current, prior and change calculations. It then returned "Unable to verify change calculation", with the limitation "Cannot produce change result without confirmed calculation tool output." The output it said it needed was already in its inherited evidence. The branch returned an insufficient-evidence answer in place of a supported comparison.

The next version would keep a validated parent answer available when a review branch fails. I would improve recovery from invalid outputs and verify that the final sentences preserve the chosen values, signs and source scope before adding more roles. This bounded setup used one model and short request budgets. It does not establish that review generally harms answers.

Follow a question through the implementation

The interface exposes the actual SQL, bound parameters, returned rows and calculated results. Evidence-backed claims link to source figures, summaries or calculations. Statements about unavailable evidence can instead explain the boundary without pretending to have a supporting figure. A reviewer can follow the working without trusting the final paragraph first.

You can run the same comparison tool directly. From the Python project folder, after installing its requirements:

from backend.finance_tools import FinanceTools
 
with FinanceTools() as tools:
    result = tools.execute("calculate_comparison", {
        "kind": "metric_change",
        "section": "income",
        "metric_id": "operating_income",
        "current_period": "2025-Q4",
        "prior_period": "2024-Q4",
    })

This reads the two source amounts and returns the $86 million change with its input references. No model or key is involved. The agent's job is to choose this operation when it fits the question, then explain the result without changing its meaning.

The online service is a real Python/FastAPI function behind this Next.js website. The SQL and calculated examples run there. The recorded agent experiment runs locally against a fixed free model through OpenRouter. The key stays outside the repository and is never deployed. Public model calls are disabled in code.

That separation keeps the page useful when a free provider is busy. A visitor can still inspect the report and replay the recorded experiment, including unsuccessful attempts.

Project sources & checks
Try the finance evidence desk ↗

Run the calculated examples online and inspect the actual recorded agent answers, tool calls and sources.

Original public report ↗

Novartis fourth-quarter and full-year 2025 condensed financial report. The experiment uses selected source figures and eight reviewed summaries.

Code, frozen questions and recorded results ↗

Download the Python tools, evaluation rules and sanitized run records. The local API key is excluded.

On this page
Give the agent enough freedom to make a meaningful choiceKeep the arithmetic out of the prose generatorGive both review approaches the same starting pointWhat the recorded runs showedWhat changed in two answersFollow a question through the implementationSources & checks
PythonFastAPISQLLLMsAgentsEvaluation

Connected to this

All writing
Financial review lab · Project story

Exploring a financial report with Python and FastAPI

ctxforge · Project story

ctxforge: discovering what to build before coding

Product-description comparison bench · Experiment

Comparing one model call with separate vision and writing steps

Ven PetrovSoftware engineer · Barcelona
GitHub ↗LinkedIn ↗Email ↗

Find a project or a note

18 results

Transfutur ERP · ExperienceERP for transport and logisticsAs the sole engineer, I built an ERP from zero for transport and logistics. It supported roughly €5–6 million in annual invoicing across two offices, and I kept it running for about a year.Finance evidence desk · Project storyDoes a reviewer make a finance agent more reliable?16 of 24 planned single-analyst answers passed all exact checks. Adding a reviewer reduced that to 14 and roughly doubled observed elapsed time. The recorded failures show what changed.Financial review lab · Project storyExploring a financial report with Python and FastAPII started with one income change, then opened up the same 54-page report: products, regions, cash flow, core adjustments and a dataset whose figures lead back to the source.Crude-oil narrative tracker · Project storyCrude-oil research from global news and market dataI wanted help following global events that could matter for crude oil. The prototype combines news research with calculated market and position values, while keeping recent assessments separate from the longer numeric history.MNQ time-series research · ExperimentTime-series prediction experimentsAn exploration of high-resolution market data: choosing what to acquire, checking its limits, comparing time intervals and testing whether a prediction survived later data and the cost of acting on it.NQ/MNQ backtest engine · Technical noteA missing closing bar left the simulator’s short position openThe strategy reset for a new day while a shared counter still counted yesterday’s short. A small replay shows why closing the position has to happen before clearing its state.Small-cap edge research · Project storyTurning small-cap trading ideas into repeatable researchI asked Claude Code for a fresh research system that could test named price patterns, compare later periods and carry its methodology into new sessions. One candidate shows how features, entry timing and costs fit together.Gadgeon Europe applications · ExperienceConnected-device dashboards and mobile apps at Gadgeon EuropeI worked on Ionic and Angular applications that brought several APIs into dashboards for energy monitoring and connected-device control, with releases and maintenance across multiple client versions.ctxforge · Project storyctxforge: discovering what to build before codingA context discovery framework that asks an LLM to act like a senior engineering interviewer: understand the idea, uncover requirements and agree the scope before an agent starts building.Sociedades Inactivas enquiries · Technical noteSaving an enquiry before trying to send its emailOn the Sociedades Inactivas catalogue, a visitor can ask about a company or send a general enquiry. The form separates saving that request from delivering its notification, including when the visitor retries.Astro local-services website · Project storyA local-services site assembled from city, service and FAQ recordsThe Astro site describes flooring and other local services across different cities. Shared templates check availability, read the matching content and select one FAQ set for both visitors and structured data.3D Spaces website migration · Project storyMoving 3D Spaces to Next.js while its email stayed on the old serverI rebuilt the virtual-tour website with AI coding collaborators, preserving its content and URLs. The cutover also had to protect the mailbox, enquiry flow and existing analytics history.Capture CRM · Project storyConnecting classified listings to contacts in a small CRMA Chrome extension and Next.js dashboard bring classified listings, platform profiles and contact history together. Saving a listing again can add the person who was missing from an earlier import.Product-description comparison bench · ExperimentComparing one model call with separate vision and writing stepsA small product-description tool lets me inspect the visual analysis before the copy is written, change the model used for each step and compare request measurements.Between Us · Project storyBetween Us: why I kept the first take and widened the shotI compared dialogue takes, asked for wider shots and more physical acting, then changed the workflow when its studio limited the scenes I wanted to make.Restaurant-service voice agent · ExperimentDesigning a restaurant-service call around the real offerI worked through a Spanish outbound agent prompt, corrected a free visit to the actual paid service and compared how two drafts handled transfers and bookings.Vapi and Telnyx voice integration · Technical noteThe Vapi agent could speak, but it could not hear me clearlyMy Telnyx number connected to the assistant, but the caller audio was distorted. Three codec tests recovered the connection without fixing that original problem.Warloop: Echoes of Solmara · Project storyWarloop: turning generated character art into a playable prototypeGood-looking character images became tiny figures, a backwards walk and misplaced bosses in the game. I worked through the gap between generated art and playable behavior.
esc to close · Tab to move through results