Skip to contents

This is the default route through dragonfarm: drop in documents, ask them questions straight away, have a model write training questions from them, review those, and fine-tune. Nothing here needs a table of prompts and replies. If you already have one, the table route is one link away in the app and is described in the quickstart.

Everything below also happens in the app: dragon_app() opens on the Documents tab, and the Teach card at the bottom is the review step.

1. Read the documents

PDF, HTML, Markdown, plain text, Word, spreadsheets, whole folders, and web pages all work. The text is split into chunks of about 170 words that never cross a heading, and each chunk remembers its file, section, and (for PDFs) page so answers can say where they came from.

library(dragonfarm)
options(dragonfarm.runs_dir = "~/dragonfarm_runs")   # keep sets and runs after R closes

docs <- dragon_documents(c("policies/", "https://example.com/help"), name = "support")
docs
#> Document set support
#> • returns.pdf: 9 chunks, 1,320 words
#> • warranty.docx: 4 chunks, 610 words
#> • https://example.com/help: 11 chunks, 1,540 words
#> 24 chunks of about 170 words; index: BAAI/bge-small-en-v1.5

Two things happen quietly. Quoted passages and code blocks are marked on their chunks, and files that look like a changelog (NEWS, CHANGELOG, or mostly version headings) are flagged and down-weighted. Both came out of testing: a quoted example reply in a README turned into confident, wrong training questions about a product that does not exist, and changelog entries kept crowding out the page that answered the question. Reading the same set again re-reads only files whose contents changed.

2. Ask, before training anything

ans <- dragon_ask(docs, "How long is the return window?")
ans
#> You can return any device within 30 days of delivery, opened or unopened [1].
#>
#> [1]* returns.pdf, p. 2 › Returns
#> [2]  returns.pdf, p. 3 › Refunds
#> [3]  https://example.com/help › Orders
#> [4]  warranty.docx › Coverage
#> * cited in the answer. Excerpt text is in $sources$text.

The k most relevant chunks, found by combining the meaning of the question with its exact words and figures, go in front of a model as numbered excerpts with an instruction to answer only from them and cite them. The model is whatever dragon_check() recommends for your GPU memory: a 1.5B model under 6 GB, a 3B at 6 to 8 GB, a 7B loaded in 4-bit at 8 GB and up. Pass model = to use a run, another id, or a server backend. Models of 3B and up answer in full, with the figures, steps, or code the excerpts hold; smaller ones answer in a sentence or two, because asked for more they made things up. In the app the answer appears as it is written (on_token = does the same in code).

This step is worth doing first for two reasons. It shows you whether the documents came through (if the answer cites the wrong file, look at dragon_chunks(docs)), and for many uses it is already enough: swap the documents, ask again, no training.

A conversation works the same way, with the history kept plain:

chat <- dragon_chat(documents = docs)
chat$say("How long is the return window?")
chat$say("And if I already set the device up?")
chat$sources()      # the excerpts behind the last reply

3. Write training questions, then review them

qa <- dragon_questions(docs)             # the recommended local model, in the background
#> 72 questions written, 41 kept by the filters (57%). Review them before training.
#> Rejected: answer not grounded (19), names not in the source (7), question length (3), duplicate question (2)

For each chunk a model writes three self-contained questions and answers them from that chunk alone: by default a mix of a “what”, a “why” or “how”, and, where the passage compares two things, a “how do they differ” (mix = FALSE asks for any three). Simple deterministic filters then mark pairs whose answer is not grounded in the chunk, that name things the chunk does not, that refer to “the passage”, or that duplicate another question. Nothing is thrown away: every pair comes back with keep and a reason.

Review them. This is the step that decides whether fine-tuning helps. On the package’s own documentation, a 7B model’s pairs were about 60 percent usable, a 3B’s about 33 percent, a 1.5B’s about 20 percent, and roughly a fifth of what passed the filters was still wrong or trivial. A model fine-tuned on unreviewed pairs learned to sound sure when it was wrong.

qa[!qa$keep, c("question", "reason")]          # read the rejections
qa$keep[qa$id == "p0017"] <- TRUE               # rescue one
qa$keep[qa$id == "p0030"] <- FALSE              # drop one
dragon_questions_save(qa)                       # decisions are kept with the set

A stronger generator gives a higher yield. Any teacher works:

qa <- dragon_questions(docs, generator = dragon_llm_anthropic())   # needs ANTHROPIC_API_KEY

4. Fine-tune with the excerpts in the prompt

ds  <- dragon_questions_dataset(qa, docs)       # kept pairs, excerpts retrieved into every prompt
run <- dragon_train(ds, "Qwen/Qwen2.5-0.5B-Instruct", wait = TRUE)

Each training prompt is the retrieved excerpts followed by the question, retrieved by the question rather than taken from the chunk it was written from, so training matches what happens at answer time; the chunk the pair came from is always among them, and each answer ends with that excerpt’s number, so the tuned model keeps citing the way dragon_ask() expects (on R’s own manuals a model trained on uncited pairs stopped citing altogether). This matters more than it sounds. In the prototypes, a model trained this way recalled 89 percent of the facts in a held-out test against 68 percent for a plain fine-tune, and when the documents were edited after training it followed the new text in 88 percent of answers while the plain fine-tune kept repeating the old policy. The model learns the style and how to use excerpts; the documents stay the source of truth, and they can change without retraining.

5. Use it

dragon_ask(docs, "How long is the return window?", model = run)
dragon_chat(run, documents = docs)$say("A customer wants to return an opened device.")
dragon_merge(run, "models/support-0.5b")        # a standalone model that loads with plain transformers

What to expect, honestly

  • Retrieval does most of the work. Without it a small model answered none of our test questions correctly and invented function names; with it, the same model answered most of the questions whose answer had been retrieved.
  • Model size matters for answering, mostly in how a model fails. On 55 questions with known answers (an annual report and R’s manuals), the 7B in 4-bit was right on all of them; the 3B and 1.5B were wrong or empty four and six times, the 0.5B 14 times. On 20 questions the documents do not cover, the 7B, 3B, and 1.5B said so every time, the 0.5B 16 times. The 4-bit 7B uses about the memory of a full-precision 3B. All the numbers, including fine-tuning and a fixed-format task, are in Choosing a model size.
  • Fine-tuning on a few dozen unreviewed machine-written pairs barely helped a 0.5B and made its mistakes confident. Review, or use a stronger generator, before you train.
  • Know what retrieval cannot do. It answers from the four or so excerpts it finds, so a question that has to look at every row of a spreadsheet or every year of a table (“which state had the highest murder rate?”, “in which year did margins peak?”) is out of reach; ask those in R on the data itself. Tables are flattened into lines of text; where a table puts the years in a header row, each figure is labelled with its year (“Net sales (2024: $249,625; 2023: $237,710; …)”), which took questions about a specific year’s figure in an annual report from 5 of 10 right to 9 (PDF) and 10 (Word). Tables without such a header can still be misread. Scanned PDFs have no text to read: they are reported by name, and need OCR (for example ocrmypdf) first. A PDF that needs a password to open says so; add an unlocked copy. Numbers inside charts and maps are usually lost.
  • On real PDFs it holds up. R’s own manuals (An Introduction to R as PDF plus the R FAQ as HTML, 425 chunks) read and indexed in 41 seconds; the 7B in 4-bit answered nine of ten questions from the right section and said “the documents do not cover this” on the tenth; the generator wrote 796 pairs in 21 minutes of which 78 percent passed the filters. A 0.5B trained on the kept pairs learned to cite and to answer in one sentence, and named the right function more often (27 of 32 R questions against 22 untrained), but it also made up an answer, with a citation, to every question the manuals do not cover. The model size you can run matters more than the training.
  • Fine-tuning a 7B is possible on 8 GB with dragon_hardware(load_in_4bit = TRUE) (QLoRA), but only just: batch 1, 512 tokens, gradient checkpointing, about 22 minutes for three epochs on 190 short tickets, 7.9 GB peak. It is worth it when it fits: on held-out tickets the three-epoch 7B reproduced every policy the 0.5B had got wrong or blended. One epoch was not enough. Document prompts of about a thousand tokens also fit now, slowly: three epochs on 616 pairs from R’s manuals took 4.3 hours (batch 1 x 16, 1,536 tokens, gradient checkpointing). The tuned 7B was as accurate as the untrained one on ten test questions (nine of ten), answered more briefly, cited an excerpt every time, and was never confidently wrong; a tuned 0.5B on the same pairs was wrong on about half of those ten. For speed, a cloud GPU still does this in a fraction of the time.
  • PDFs come through as text, not layout. Where a table puts its years in a header row, each figure is labelled with its year; other tables can still be misread. Markdown, HTML, Word, and spreadsheets come through cleanly.
  • The first question in a session loads the model. For a 7B in 4-bit that is about a minute; later questions take a few seconds.

The numbers above come from one corpus (this package’s own documentation), 35 hand-written questions, and one run each. They are measured, not promised; measure on your own documents. The scripts are in scripts/doc_prototype/ and scripts/rag_prototype/ in the repository.