Ethan Chen
All work

FirmGround Agentic RAG

A document question-answering system for regulated, document-heavy work. It finds the passages that answer a question, does any arithmetic the answer needs, and cites the exact table or paragraph.

Role
Design and engineering
Status
Piloting as the history knowledge base of a public junior secondary school in Tianjin
Stack
FastAPI, Taskiq, PostgreSQL, Redis Streams, OpenSearch, Qwen3 reranker
Demo, 1:33. The agent reconciles two figures in a 3M 10-Q, checks the arithmetic in code, and opens the cited table, then answers from audit standards and a textbook. The walkthrough below follows a different question, from the audit library.

1The question

This page follows one real question from start to finish. It comes from a library of audit standards. Section 4 then measures the same system on financial filings.

Midyear, the team concludes that controls over revenue are ineffective. The manager says, “We’re testing 100% of revenue transactions over $50K, so we can lower our assessed risk of material misstatement for revenue.” Evaluate this statement.

This question is harder than it looks, for two reasons:

  • The answer is spread across several standards. How audit risk is defined (AS 1101), how auditors respond to assessed risk (AS 2301) and how they evaluate audit results (AS 2810) are separate documents. A system that finds one relevant passage and stops will have only part of the answer.
  • The manager’s claim sounds reasonable. More testing feels like less risk. A useful answer has to separate what testing changes from what it doesn’t, and point to the paragraph that says so.

2Watch the agent work

FirmGround does not answer in a single step. It runs as an agent: a language model that decides what to do next, uses tools such as search and a code runner, reads the results, and keeps going until it has enough to answer.

  1. It searches each standard separately

    The run took 18 steps. Instead of one broad search, the agent searched the library several times, one topic at a time. Two of those searches are below, on AS 2810 and AS 2301. Other searches in the same run found AS 1101.

    Two search steps: five sources about AS 2810, and five sources about AS 2301
  2. It gives a verdict, with reasons

    The manager has it backwards. Weak controls push the company’s side of the equation up, and testing only pulls the auditor’s side down. So the assessed risk should go up, and the extra testing is the right response to that, not a reason to lower it. Every claim carries a numbered citation.

    The verdict, that the manager's statement is incorrect, and the first part of the reasoning with numbered citations
  3. Every citation opens its source

    Citation 28 opens AS 1101 at page 15, with paragraph .04 outlined. It is the sentence the answer rests on: audit risk is a function of the risk of material misstatement and detection risk.

    AS 1101: Audit Risk, open at page 15, with paragraph .04 outlined

Each step on its own is simple. What matters is the loop: the agent checks whether it has enough evidence before it answers, and keeps searching if it does not.

3Under the hood

The question path

  1. Question
  2. Rewriteinto searchable sub-questions
  3. Hybrid searchkeyword + semantic, RRF, rerank
  4. Enough evidence?
  5. Computein the sandbox, if needed
  6. Answerwith citations

Not enough evidence: rewrite the query and search again.

model decides

The compute step only runs when a question needs arithmetic, such as FinanceBench’s calculation questions in Section 4. The agent writes a short calculation and runs it in a sandbox, an isolated computer that runs its code without touching anything else, instead of doing the arithmetic in its head. The audit question above needed none.

At ingestion, every document is split into small passages for matching. Each passage keeps a link to the larger section it belongs to, so the agent reads results with their surrounding context and section headings. Search then runs in three stages:

  • Two kinds of search run side by side. Keyword search (BM25, the ranking method behind most search engines) is good at exact terms such as a standard number or a line-item name. Semantic search compares meaning, so it finds relevant passages even when the wording differs from the question.
  • The two result lists are merged by rank. The two methods score passages on unrelated scales, so their scores can’t be added. Reciprocal rank fusion (RRF) merges the lists by each passage’s position in each.
  • A reranker makes the final cut. A Qwen3 reranker reads the question with each candidate passage, scores how well it answers, and the best few go back to the agent.

Search only covers documents the user is allowed to see, and every result carries a link back to its place in the original.

Ingestion and streaming

Uploading a 150-page PDF shouldn’t block the page. The upload returns at once. A background worker parses the PDF into text and tables, splits it into passages, computes their numeric representations and adds them to the search index.

FastAPI
The web API. Everything runs asynchronously, so one process can wait on many slow operations at once.
Taskiq
Background jobs, built for asynchronous Python. Runs both ingestion and the agent itself.
PostgreSQL
The system of record for documents, versions, permissions and conversation history.
Redis Streams
The job queue, and the live feed of each answer while the agent works.
  • No lost uploads. Every upload is recorded in the database before it is queued. If a job is lost, a scheduler finds it and sends it again.
  • Answers survive a refresh. Answers stream to the browser as the agent works. If the connection drops, the page reconnects and picks up from the last event it received, and the agent keeps working in the meantime.

4Does it hold up?

Accuracy on FinanceBench

FinanceBench is a public benchmark for financial question answering, published by Patronus AI. Its open sample has 150 questions over 84 real filings: 10-Ks (annual reports), 10-Qs (quarterly reports) and earnings releases (the short results announcements companies publish each quarter) from companies such as 3M, Amazon, Boeing and JPMorgan. The questions come in three groups of 50: questions an analyst would ask, figures that must be calculated from line items, and open-ended questions written for the dataset.

I loaded all 84 filings into one shared library, so every question searches the whole corpus instead of being handed the right document. While checking the reference answers, I found four that contradicted their source filings. I corrected those four from the documents and labelled them as corrected.

I compared three systems. Naive RAG runs one search, then answers from whatever came back, using the same language model and embedding model as FirmGround, so the gap measures the agent loop, not a better model. A closed-book model answers from memory with no documents, as a floor. A refused question counts as a failure.

FinanceBench open sample, 150 questions
SystemCorrectIncorrectRefusedAccuracyAccuracy when it answered
FirmGround Agentic RAG12226281.33%82.43%
Naive RAG321410421.33%69.57%
Closed-book model36516324.00%41.38%

Two things stand out. First, naive RAG scores below the closed-book model. When it did answer, it was right about 70% of the time. Its problem is that it refused 104 of 150 questions, because a single search usually didn’t bring back enough evidence.

Second, the gap is widest on calculation questions, which need several line items and arithmetic. On those, FirmGround scored 88% and naive RAG 2%. FirmGround’s weakest group was the open-ended questions, at 74%.

Accuracy by question type, 50 questions each

Domain-relevant

FirmGround82%
Naive RAG24%
Closed-book38%

Metrics-generated

FirmGround88%
Naive RAG2%
Closed-book20%

Novel-generated

FirmGround74%
Naive RAG38%
Closed-book14%

Ingestion speed and memory

I uploaded four Chinese history textbooks at once, 585 pages and 133 MiB of PDF, and timed each until it was searchable, on a single background worker running on a laptop.

Throughput
372 pages/min

All four books searchable after 94 s. Median book ready in 58 s.

Peak memory
~650 MiB

Whole worker container (644 MiB measured). The worker process peaked at 449 MiB.

Most of each book’s time, 79–90%, went to parsing the PDF on an external document-parsing service. Splitting, embedding and indexing took about ten seconds per book. The largest file (74 MiB) is parsed in three parts, one after another. I chose that to keep memory bounded. Parsing the parts in parallel would be faster, at the cost of higher peak memory.

This was a single run on one machine. Treat it as a measured data point, not a capacity guarantee.

5Other libraries

Nothing in the system is specific to one field. The same pipeline has been used on very different material.

Audit. Besides the standards library above, an IPO audit scenario keeps two libraries apart: PCAOB and SEC standards in one, a fictional client’s filing materials in the other. The agent cites both, so a reviewer can check the rule and the evidence side by side.

Education, in use. FirmGround is piloting at a public secondary school in Tianjin, where teachers use it as a knowledge base for history.

6Choices I’d defend

Why not put the whole document in the model’s context?

  • Size. A single 10-Q in FinanceBench runs to about 59,000 words, and the question doesn’t come with the right filing attached: the answer could be in any of 84. You can’t put the whole library in front of the model for every question.
  • Cost and speed. You pay for every word the model reads, on every question. Search narrows that to a few passages.
  • Citations. A model reading a whole document can tell you an answer, but not reliably where it found it. Retrieved passages become the citations a reader can check.
  • Permissions. Search only returns documents the user may see. Pasting whole documents into a prompt skips that check.

Why not a single retrieval pass?

That is the naive RAG baseline, at 21.33%. A single pass fails in a specific way: it doesn’t get the reasoning wrong, it never finds all the evidence. The audit question draws on several standards, and a calculation question needs figures from several sections. One query rarely brings them all back. The agent loop, which checks for missing evidence and searches again, accounts for most of the gap between 21% and 81%.

Why Taskiq?

The backend is asynchronous from top to bottom, and Taskiq is a task queue built for asynchronous Python. Background jobs reuse the same code as the web API. Celery, the more established choice, is designed around blocking functions, so asynchronous code inside it needs extra glue in every task.

Taskiq also runs on Redis Streams, which I already use for live answers. A worker must confirm each job when it finishes, and an unconfirmed job is handed out again. Agent turns can run for a long time, so the default 10-minute limit would start a second copy of an agent that is still working. I raised it to 6 hours.

7What I’d build for a desk

For an analyst desk, the next step is a tool that compares a company’s new filing with the previous one, flags changes in risk-factor language and guidance, and cites every flag in both documents. Most of the pieces already exist:

  • Sections line up. Ingestion already keeps each passage’s section heading, so this quarter’s “Risk Factors” can be matched against last quarter’s.
  • Real changes, not rewording. A section-by-section comparison finds passages that were added, removed or reworded. The agent sorts them into real changes, such as a new risk, a softened forecast or a changed number, and routine rewording.
  • Two citations per flag. The old passage and the new one, so the analyst can confirm or dismiss it in one view.
  • Measured before anyone relies on it. Build a set of filing pairs with known changes, then measure how many the tool catches and how many of its flags are noise.

The output is a review list for an analyst to confirm, not an automatic alert. The desk keeps the judgement, and the tool does the reading.