Skip to content

The chunks of one document are extracted one at a time #588

Open

Description

@WaylandYang

One document is one job, and inside the job the chunks are extracted one after another (extraction.rs, for chunk in chunks.iter()). A model call takes about 55 seconds on DeepSeek-V3 with the schema.org candidates, so a document's extraction time is chunks × 55 s no matter what the deployment's worker_concurrency or the model's max_concurrent say: those only let more documents run at once.

Measured on the #582 runs, three documents uploaded together: anthropic.txt (27 chunks) 25 min, removal-of-sam-altman-from-openai.txt (23) 21 min, openai.txt (73) 68 min. The two small ones finish and the base then waits three quarters of an hour on one document with two idle workers, while the model gate (10 concurrent by default) sits at one call.

What should happen

The chunks of one document are extracted concurrently, bounded by the model's concurrency gate (model_limits, the same one every other call goes through), and the results are applied in chunk order so that entity resolution and the ledger behave exactly as they do today: resolution is order-sensitive (a later chunk's namesake logic reads what earlier chunks bound, handled_by_name, response_claims), so the model calls are the parallel part and the resolution stays sequential over the replies in sequence order.

Expected effect on the same three documents: bounded by the gate, not by the longest document. With a gate of 10, openai.txt becomes roughly 73 / 10 × 55 s ≈ 7 min of calls plus resolution.

Not this

  • Not a bigger worker_concurrency: it changes nothing for a single document.
  • Not parallel resolution: the order-dependent parts stay in order.
  • Not a change to what is extracted: the same prompt, the same replies, the same ledger; a run before and after on the same documents should differ only in wall-clock time (and the run-to-run variance the model already has).

Acceptance

  • Three documents uploaded together finish in about the time of the longest document's chunks / gate calls, not chunks calls.
  • A failed chunk is reported the same way it is today (unextracted, the per-document summary), and a rate-limited call still backs off through the gate rather than retrying in a tight loop.
  • The extraction ledger and the fact/entity counts on a re-extraction are within the run-to-run variance already measured (A fact quotes the words that name each side #583's table).

Related: #526 (a worker held while the ontology index is built, the other reason a document waits), #583 (the runs that measured this).

Activity

  1. jeremycapps commented on Sep 15, 2026

    @jeremycapps
    Contributor

    Hey @WaylandYang, I saw here that re-extraction is being reworked. Does that cover single-document chunk concurrency, or is this separate? If it's separate, I'd like to help plan it. Here's how I'd scope the first step.

    Scope

    First PR is a refactor only: make known a settable input to the extraction call. Concurrency and the variance measurement build on it and come after.

    The refactor

    Split the per-chunk loop into a call/parse step and an apply step, along the boundary that already sits at the model call. No behavior change on its own: same order, same prompts, same ledger, existing extraction tests green.

    • The loop body becomes call-then-apply as two helpers: L1101.
    • The call/parse helper takes context, ontology lists, the known build, messages, and the model call: L1108-L1159.
    • known becomes an argument to it, current doc_entities by default or &[] behind a flag: L1126-L1134.
    • The apply helper takes resolution, name recording, fact and evidence writes, the doc_entities append, and mark_chunk_extracted: L1272-L2803.
    • The prompt render stays untouched; the flag changes what known contains, not how it renders: lib.rs L507-L541.

    Why this first

    The open question is how parallel one document's chunks can go, and it hinges on one number: does dropping known move extraction output beyond the run-to-run variance in #583's table. The refactor is cheap and low-risk; the concurrency shape after it depends on that answer. Measure, then decide, so we don't build ordering machinery the number might not justify.

    Dependency

    The variance run needs a stable baseline. #714 changes doc-date handling on the same recall-bench corpus #583 measured, so take the known comparison before #714 lands or on a subset it doesn't touch, or the two deltas confound.

  2. rollroyces commented on Sep 20, 2026

    @rollroyces
    Contributor

    Picking this up. Jeremycapps's comment in this thread already scoped cut 1 as the refactor only: make known a settable input to the extraction call, with the chunk loop split into a call/parse helper and an apply helper. No behavior change. Both #714 and #583 are now CLOSED and MERGED respectively, so the variance-run dependency is gone — the refactor can land.

    The minimum-cut I have in mind:

    1. Extract a call_and_parse_one_chunk(...) -> ChunkOutcome that takes the same arguments the current loop body builds each iteration (known: &[KnownEntity], messages context, the chunk, opening chunk override, etc.) and returns Parsed { extraction, cut_by_ceiling } or Skipped(reason) or Stolen (epoch was bumped). The model call, the rate-limit retry, the parse, and the two audit-drop paths (CHUNK_UNEXTRACTED for model failure and parse failure) all move into this helper.
    2. Extract an apply_open_extraction(...) -> () that takes the parsed reply plus &mut references into the order-sensitive HashMaps (doc_entities, known_by_name, described, handled_by_name, ambiguous_bare_cache, touched_names, statement_count, needs_adjudication, human_reviews_found) and runs the existing apply half: entity resolution, the place phase for deferred entities, fact writes, evidence writes, the truncated/malformed drop signals.
    3. The loop body itself becomes: epoch check, build known from doc_entities, call helper, match on outcome, apply or unextracted.push, then mark_chunk_extracted. The order-sensitive state still mutates sequentially per chunk — concurrency is a separate cut.

    Two questions before I write the patch:

    1. Parameter shape on call_and_parse_one_chunk. I see three defensible shapes:

      • (a) Many arguments: (state, settings, client, pool, kb_id, doc_id, my_epoch, chunk, opening, known, proposer, bound_types, attested_at) — explicit, no hidden state.
      • (b) A CallContext struct built once before the loop holds the run-level fields; the helper takes (ctx, &mut chunk, opening, known) — smaller per-call surface, but adds one allocation.
      • (c) The helper takes &mut self on a struct that owns the LLM client and a small cache — only worth it if we want to keep call state between chunks.

      My read: option (a) is the smallest cut. Option (b) is more readable if the function grows (the next cut is concurrency + per-call cache, where the struct pays off). I'd ship (a) today and refactor to (b) when the concurrency cut lands.

    2. Tests. The current extraction_open.rs test module is pure-function tests (span_in_quote, locate_time); no integration test of the chunk loop. Should this PR add a test? The cleanest would be a doc-backed test that exercises run_open end-to-end with a scripted HTTP model (Maya-Kid did this for An ontology edit during type alignment can be hidden by the response timestamp #795, with a fixture postgres), but that's a fixture setup of its own. Two options:

      My read: (a). Adding the test expands scope by another 100-200 lines and a fixture setup; the refactor itself is provably behavior-preserving by reading the diff. Maintainer's test-the-test approach (run on a real base, eyeball the diff) is the right gate for this cut.

    I'll wait on (1) before writing code. (2) is a heads-up — if you want a test, say so and I'll add it; if you don't, I'll ship without.

    What I would NOT do in this PR

    • No concurrency. Jeremycapps's call was: refactor first, measure the variance, then decide whether concurrency buys anything. The cut is behavior-preserving; the existing for chunk in chunks.iter() stays sequential.
    • No extraction_prompt change. The flag changes what known contains, not how it renders. The build_open_messages function is untouched.
    • No schema change. No migration.
  3. WaylandYang commented on Sep 25, 2026

    @WaylandYang
    ContributorAuthor

    Cut 1 as you scope it is right: a refactor with no behaviour change, known as an explicit input, the loop body split into a call-and-parse helper and an apply helper. One update since the thread was written: the loop moved. Extraction now writes the open graph, and the chunk loop lives in crates/utopia-server/src/extraction_open.rs (run_open, the for chunk in chunks.iter() there), not in extraction.rs; it also has a pushed branch that parses the chunk instead of calling the model (#884). The helper should take that branch with it.

    For cut 2, when chunks of one document run at once, the per-model concurrency gate in llm_util stays the only limiter, and known keeps its meaning: an entity listed by an earlier chunk is known to a later one, so the apply step has to run in chunk order even when the calls do not.

  4. jeremycapps commented on Sep 25, 2026

    @jeremycapps
    Contributor

    Thanks @WaylandYang, and thanks @rollroyces for picking up cut 1.

    Once cut 1 lands, I'd like to take the measurement it unblocks, and then cut 2 if the numbers support it.

    Measurement

    Take the #583 corpus and model config and run each document with known as today (earlier chunks' doc_entities) and with known empty. Do several runs of each so the comparison has its own variance and doesn't lean on #583's alone. Report fact and entity counts, the ledger diff, and namesake/handled_by_name resolutions per document, next to #583's table. The question is the one from before: does an empty known move the output beyond the run-to-run variance? The answer decides how much of cut 2 can go in parallel.

    Cut 2

    As you described it: the calls run at once under the llm_util gate, rate-limited calls back off through the gate, and the apply step consumes replies in chunk order. I'd measure wall-clock time on the same three documents from the issue before and after, so the acceptance check (about the time of chunks / gate calls) is a measured number.

    Two questions before I start:

    Is the #583 corpus and model config still the right baseline now that #714 has merged, or should I rerun a fresh baseline first?
    Should the measurement go in a comment here, or as a script/bench in the repo so it can be rerun later?

  5. WaylandYang commented on Sep 25, 2026

    @WaylandYang
    ContributorAuthor

    Answering the four open questions in order.

    @rollroyces, on the refactor:

    1. Parameter shape: (a) today, (b) when cut 2 lands, exactly as you read it. The run-level fields (state, settings, client, kb, document, epoch, bound_types, attested_at, pushed) are the ones that would go in the context struct; keep them as plain arguments now so the diff is the split and nothing else.
    2. Test: add one, but not a new fixture. errata_tests.rs and phrase_alignment_tests.rs already drive a real database with a scripted HTTP model (Fx::new(vec![script]), replies in order); a test that ingests a two-chunk document, scripts two open-contract replies where the second chunk names a thing the first listed, runs extract_document, and asserts the second chunk resolved to the first chunk's entity is about eighty lines on that pattern. It is the test that cut 2 needs anyway: it pins that known from an earlier chunk reaches a later one, which is the property concurrency must keep. Without it, "behaviour-preserving by reading the diff" is the only gate, and the diff is a thousand lines moving.

    @jeremycapps, on the measurement:

    1. A fact quotes the words that name each side #583 is not a baseline any more. It measured the typed extractor; extraction now writes the open contract (0044 cut 1, A statement keeps the document's words #731 and Extraction reads only the open graph #736), so counts are not comparable. Rerun a fresh baseline on current dev with scripts/bench/recall.mjs on its corpora, several runs, and that table is the variance the known comparison is read against.
    2. In the repo, not a comment: a --known empty knob on recall.mjs (it needs a server-side switch, which is the flag the refactor introduces) and the numbers in scripts/bench/README.md next to the other measured runs, with the summary posted here. A comment cannot be rerun after the next change to extraction, and there will be one.

    Sequence: rollroyces lands cut 1 with the switch and the test; jeremycapps runs the baseline and the comparison on it; cut 2 goes in only if an empty known moves the output beyond the run-to-run variance by less than the wall-clock gain is worth, or with the apply step kept in chunk order if it does not.

  6. jeremycapps commented on Sep 28, 2026

    @jeremycapps
    Contributor

    Cut 1 is in (#980), thanks @rollroyces. The next step is the known measurement, and I'd like to split it: I've built the bench changes, but I don't have a budget for model calls, so I'm asking whether someone with the bench setup can run the rounds.

    What's ready: recall.mjs changes (#1002)

    • --known shown|empty: a label for the run. The switch itself is UTOPIA_EXTRACT_KNOWN_IN_PROMPT, set when the server starts, so each group runs against a server started to match.
    • --out <dir>: one JSON record per run. Per document it records chunks, facts, entities, entities that appear in facts from two or more chunks, name_vector| pairs proposed to the adjudicator (Recall a mention by its name vector so a short form or another script meets its entity #877), drops by reason (including chunk_unextracted), and seconds from queueing to the last chunk. It also records the knowledge-base-wide pair total, split into pending / merged / kept.
    • --table <dir>: groups runs by label and adds the fact-set overlap within each group (the run-to-run variance) and across the two groups (the effect of known).

    I counted the name_vector| pairs because since #877 a variant name becomes a pair for the adjudicator rather than a silent merge. So an empty known may show up as more pairs, with the recall score barely moving. The score alone could read as "no difference."

    Not yet run against a database. I don't have a model endpoint, so the queries in stats() and nearPairs() have only been reviewed, not run. --table is tested on made-up records.

    The ask

    Could someone who has the bench running do three rounds with known shown and three with it empty, on current dev (after #991/#995/#1000, since all three changed the prompt)?

    node scripts/bench/recall.mjs --kb <id> --known shown --out runs/588   # ×3
    # restart the server with UTOPIA_EXTRACT_KNOWN_IN_PROMPT=false
    node scripts/bench/recall.mjs --kb <id> --known empty --out runs/588   # ×3
    node scripts/bench/recall.mjs --table runs/588
    

    Any model is fine as long as all six rounds use the same one. I'd guess gemini-3.5-flash with reasoning_effort minimal, as in the recent runs. If the first round shows a problem with the queries, I'll fix it the same day. I'm happy to write up the table in scripts/bench/README.md and post the summary here, so all anyone needs to share is the runs/588 records. Or, if there's a project key contributors can use, I can run it myself.

    For cut 2, whichever way the numbers go

    a_thing_listed_by_an_earlier_chunk_reaches_a_later_one_via_known asserts that chunk 2's prompt contains chunk 1's entity, and fully concurrent calls break that by design. If the numbers support full concurrency, I'd change that test to assert only the apply-side property (the second test's), and say why in the PR. If they don't, cut 2 extracts the first chunk(s) before the rest, and the test stays as it is.

  7. WaylandYang commented on Sep 29, 2026

    @WaylandYang
    ContributorAuthor

    Results of the known measurement, @jeremycapps. Thank you for the bench (#1002, #1005); it ran as written.

    Three rounds per group (edited 2026-09-30: the first version of this comment had two per group; the two rounds that were void on the first day were re-run). Rounds alternate shown / empty with a server restart before each.

    Setup

    • dev @ 40397a5 (rounds 1–4) and c7ac51a (rounds 5–6; the two commits between touch only hexBinary import and the CI file, not extraction), one base with the schema.org pack (915 classes, 1559 properties), governance on.
    • gemini-3.5-flash (reasoning_effort minimal) and bge-m3. Rounds 1–4 through one gateway, rounds 5–6 through OpenRouter after the first ran out of credit; same models, and the bge-m3 vectors were checked against the ones already in the base (cosine 0.9999).
    • 4 SEC filings, 55 chunks, all extracted in every round.
    • One warm-up round before these, not counted.

    Rounds

    Round known Score Pairs proposed Pending at settle Merged
    1 shown 48/52 228 0 13
    2 empty 48/52 267 0 23
    3 shown 47/52 208 89 5
    4 empty 47/52 309 126 17
    5 shown 47/52 372 6 20
    6 empty 47/52 244 150 6

    Pending pairs are governance calls the gateway refused (rate limits on the second day, an outage on the first); the pair totals are counted at proposal and are not affected, the merged counts are.

    Fact-set overlap (Jaccard, averaged over documents)

    Overlap
    Within shown (3 pairs) 48.5 ± 3.9% (44.6–52.3)
    Within empty (3 pairs) 46.9 ± 1.2% (45.7–48.1)
    Across (9 pairs) 34.3 ± 3.9% (29.4–39.6)

    All nine across-group pairs sit below all six within-group pairs.

    What it says

    1. Recall does not move. 48, 47, 47 in both groups; the misses are the same four items in every round (cfo-title, er-coreweave, er-nebius, er-oci) plus one that varies.
    2. The output does move beyond run-to-run variance. Two runs of the same configuration share about 48% of their facts; a shown run and an empty run share about 34%. The gap is 14 points and the ranges do not touch.
    3. The number of pairs proposed to the adjudicator does not separate the groups. After four rounds it looked as if empty known proposed more (this comment said so); round 5, a shown round, proposed 372, the most of any round. Shown: 228, 208, 372. Empty: 267, 309, 244. The count follows how much the model extracted from the earnings release in that run (366 of round 5's 372 pairs are that document's), not known.

    For cut 2

    Running the calls of one document at once is safe for recall. Without known, a third of what a run writes is different from what it would have written with it, and the adjudication count does not show a cost. So the decision is about which output we want, not about recall or adjudication load. Keeping part of known under concurrency (the first chunk alone, the rest at once with what it listed) is still on the table; I would like your view before deciding.

  8. jeremycapps commented on Sep 29, 2026

    @jeremycapps
    Contributor

    Thanks for running these, @WaylandYang.

    My view: keep part of known, and the simplest part covers most of it. Extract chunk 1 alone, then the rest at once with chunk 1's known, applying in chunk order as today.

    Why chunk 1 carries most of it

    known only helps an entity that appears in more than one chunk, and in my round that's a small, lopsided set. The earnings release (32 chunks, 165 entities, known shown):

    Chunks an entity appears in Entities
    20 1 (NVIDIA)
    3 5
    2 10
    1 149

    The one entity that runs through the document is its subject, and chunk 1 defines it (NVIDIA Corporation ("NVIDIA" or the "Company")). Seeding from chunk 1 keeps that spelling stable everywhere. What it gives up is the handful of 2–3-chunk names first introduced later (Vera Rubin, the non-GAAP measures).

    What it costs

    One extra round. The same document took 228 s sequentially (gemini-3.5-flash, minimal, about 7 s a chunk). With chunk 1 first and the gate at 10 it would be about 7 + 28 ≈ 35 s. Apply stays in chunk order and the list is still built from resolved entities, so the invariant from the issue holds, and a_thing_listed_by_an_earlier_chunk_reaches_a_later_one_via_known still holds for chunk 1 onward.

  9. jeremycapps commented on Sep 29, 2026

    @jeremycapps
    Contributor

    A follow-up to my comment above, @WaylandYang: the alternatives I looked at before landing on the chunk-1 seed, and why I'd still start with it. Collapsed, since it's long.

    Alternatives to the chunk-1 seed, and why start with it

    Terms

    • n: chunks in the document. G: the model gate (default_model_concurrency, 10 here). t: one call, about 7 s on gemini-3.5-flash at minimal effort (includes apply; the two are not split).
    • Blind: a chunk sent with no known.
    • The chunk count is known before the first call: the whole document is chunked at ingest and all chunks are written before the job is queued.

    Alternatives

    Option How n = 32 n = 73 Blind Trade-off against the chunk-1 seed
    Chunk-1 seed (proposed) chunk 1, then the rest at once, applied in chunk order 35 s 63 s none*; the rest see only chunk 1's names Gives up recurring names first introduced after chunk 1 (13 of the 16 that recur in the earnings release) and costs one extra round (about 7 s). Keeps chunk-order apply and a list built from resolved entities. Output does not depend on the gate or on timing.
    Sequential (today) one chunk at a time 224 s 511 s none* Every name seeded, but about 6-8x slower. It is the problem this issue exists to fix.
    All queued, no known every chunk queued at once, known empty; the gate runs G at a time 28 s* 56 s* all 7 s faster than the seed, but the subject is spelled freely in every chunk: +17-50% pairs in your rounds. After the first pass, replies are already back and go unused.
    Contiguous waves 1-10, then 11-20, ... 28 s* 56 s* first wave No better seeding than the seed for the subject, and only the first chunk of each wave sees its predecessor's names. Wave size follows the gate, so the gate changes the output.
    Interleaved waves, fixed stride (s = 10) 1, 11, 21, ..., then 2, 12, 22, ... 70 s 70 s about 1/s (12.5%, 11%) Seeds the late recurring names the seed gives up, but is 2x slower at 32 chunks, needs the next wave's list built from raw replies (variants and rejected names get in) or applied by wave (breaks chunk-order apply), and changes the prompt's "earlier parts" wording.
    Interleaved waves, wave size = G stride = ceil(n / G) 28 s* 56 s* about G/n (25%, 14%) As fast as queuing everything, with the same list problem as the fixed stride, and the gate decides which chunks run blind.
    Sliding dispatch interleaved order, G in flight, each call gets every reply so far 28 s* 56 s* first G sent (31%, 14%) Best seeding at the gate's full speed, but two runs of one document see different lists depending on which calls finish first, and it has the same list problem.

    * Best in its column; tied options are all marked. The seed is the only option near the best in both time and blind chunks.

    Why start with the chunk-1 seed

    • It keeps both of the issue's invariants. Apply stays in chunk order, and the list is built from resolved entities exactly as today. Every wave scheme gives up one or the other.
    • Nothing new decides the output. No stride, no dependence on the gate, no dependence on timing. A run is as reproducible as today's.
    • The spread says chunk 1 covers most of what known does. In the earnings release, 1 of 165 entities is in 20 chunks, 5 are in 3, 10 are in 2, and 149 are in one. The one that runs through the document is its subject, and chunk 1 defines it (NVIDIA Corporation ("NVIDIA" or the "Company")).
    • It costs 7 s over queuing everything with no known, and it is the smallest change from today's code: one call before the rest, and the existing apply loop.
    • The others build on it. If the late recurring names turn out to matter, interleaving can be added on top of the seed rather than replacing it.

    What would make a wave scheme worth it

    Each of these would need an answer first:

    1. Where the next wave's list comes from. From raw parsed replies it is deduplicated only by name: a blind wave's variants (NVIDIA, NVIDIA Corporation) both enter it, as do names the apply step would reject. From applied replies it is today's list, but applying by wave breaks chunk-order apply. It is the same number of applies, grouped differently; the apply time per chunk is unmeasured.
    2. Reconciling one wave before the next. Adjudication is queued after the whole document, today included, so it never runs between waves. The cheapest fix is to collapse the list, not the entities: group listed names that are near by name vector and share a kind word, and show one spelling per group. Adjudicating each wave's pairs first is correct but puts a model call on the critical path per wave.
    3. The gate must not decide the output. A fixed stride keeps the gate to speed. The two interleaved schemes cross at n = s x G (100 chunks here); above that, a wave larger than G is split by the gate into passes that wait without seeding, so the stride has to grow to ceil(n / G) anyway.
    4. Short documents gain little. A 7-chunk document saves about 35 s and runs more than half its chunks blind (4 of 7) in any two-wave scheme.
    5. Which names are worth the list. Without domain knowledge, spread across chunks counted over name-vector groups, and variant risk (an alias in n, a trademark symbol or a legal suffix, an all-caps form, a product-family prefix). Rarity is weak: recurring names are rare (Vera Rubin) and generic (GAAP, AI) alike. The list's 1,200-character cap already drops recurring names that first appear late (GAAP operating expenses, chunk 26).

    Open questions

    • Apply time per chunk, which decides what grouping applies would cost.
    • Whether seeing later chunks' names makes the model bind a passage's mention to a name defined elsewhere.
    • How the list should be ordered and capped once it holds names from across the document.
  10. WaylandYang commented on Oct 1, 2026

    @WaylandYang
    ContributorAuthor

    Thanks @jeremycapps, for the measurement of where known actually matters and for the alternatives table. Decision: go with the chunk-1 seed. Chunk 1 alone, then the rest at once under the model gate with chunk 1's list, applied in chunk order as today.

    What settled it: the 165-entity count shows known is about the document's subject, and chunk 1 defines it. The 14-point drop in fact-set overlap that the measurement found is the cost of losing that; the handful of 2–3-chunk names introduced later is what we accept losing. One extra round for a 6× wall-clock gain on a 32-chunk document is a good trade.

    Two things to keep as you build it:

    • The apply half stays sequential and model-free, as cut 1 left it. The seed changes only which known the parallel calls receive.
    • Report the same three numbers on the same corpus before and after (recall, pairs proposed, fact-set overlap against a sequential run), so the change is measured the way known was.

    Please take it; you have the context. Open the PR against dev when it is ready, and say in it which of the alternatives you ruled out and why, in a sentence each, so the record has it.

  11. rollroyces commented on Oct 15, 2026

    @rollroyces
    Contributor

    动手前想先把并行化的边界定下来。crates/utopia-server/src/extraction_open.rs:985 的循环有跨 chunk 共享 mut 状态:

    • unextracted: Vec<(i32, String)> — 第 N 块在解析时消费前 N 块留下的 unextracted
    • time_entries: Vec<utopia_extract::time::TimeEntry> — 每块报上来的日期往里 push
    • known: HashMap<String, Vec<Uuid>> / response_claims / handled_by_name / ambiguous_bare_cache — 名字消歧的"前面认下的实体"缓存
    • extracted_chunks: Vec<Uuid> — 顺序

    extract_known_in_prompt: bool 这个旋钮(crates/utopia-server/src/state.rs:35)是 maintainer 留的"开/关 提示词里出现 known"接口,说明并行化这事早就在动念。但 known/unextracted 这两路就算旋钮关掉也仍要写:第 N 块消歧时仍可能问"我之前这块认下的 k1",而顺序无关假设这块跳过 known 是可以并发跑得稳的。

    想确认三件事再下刀:

    1. 并行粒度:futures::stream::iter().for_each_concurrent(N, ...) 还是 tokio::task::JoinSet?这一处是 extraction.rs 唯一的 for 循环,影响范围有限。
    2. 并发的预算:state.worker_concurrency 是 job 级的,不适合拿来当 per-chunk 的并发上限。LlmSettings::max_concurrent(如果存在) 还是按 chunk 数自适应?#588 没写明。
    3. 错 chunk 行为:for chunk 现在遇到 Err(e) 直接 ? 整文档失败;并发下某一块失败:整文档重 fail?部分 chunks 写库、坏的那几块写 last_error?issue 没讲。

    A 路径:for_each_concurrent + extract_known_in_prompt = true 时回退到串行(逻辑等同现状),= false 时并行(known 路径在关掉时本来就不读前块)。代码改动:for chunk 改 for_each_concurrent、结果收集改成 Vec<Result<_>> 一次性 apply。前提:你们的 extract_known_in_prompt = true 默认值今天是不是要改?默认是 true(crates/utopia-core/src/config.rs:58),所以默认行为没变,只有显式关掉的部署才看到加速。

    B 路径:for_each_concurrent 统一生效,known 这一支改成"按 chunk_seq 索引回放"——每块写自己的 known,后块读到前块的那段 known 时 block 等待。改动面更大,跨 chunk 的 wait 也加复杂度。

    我倾向 A(默认行为不变,显式 opt-in 才加速),但 maintainer 如果觉得 B 才是你们想要的(可能默认就想并行,只是旋钮还没想清楚),我就走 B。请给一句。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions