Skip to main content

agentsop-map-reduce-fanout

Decision protocol for the map-reduce / dynamic fan-out pattern in LM pipelines — "given list L, run f(item) for each item in parallel, then combine". Activates when the coder agent is about to process N items with N LM calls (per-doc summarize, per-query retrieve, per-candidate rank, parallel tool fan-out). Encodes the *when*, *how many at once*, *what to do when one fails*, and *how to reduce* — not the API of any single framework. Cross-framework: LangGraph `Send`, CrewAI parallel tasks / Flow, `asyncio.gather`, `ThreadPoolExecutor`, LlamaIndex batch retrieval.

Quellinformationen

Repository
agentsope/SkillAlchemy
Letzte Quellaktivität
20. Mai 2026 um 14:58
Erkannte Sprache von SKILL.md
Englisch
Sterne
457
Forks
25

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
agentsop-map-reduce-fanout
version
0.1.0
description
Decision protocol for the map-reduce / dynamic fan-out pattern in LM pipelines — "given list L, run f(item) for each item in parallel, then combine". Activates when the coder agent is about to process N items with N LM calls (per-doc summarize, per-query retrieve, per-candidate rank, parallel tool fan-out). Encodes the *when*, *how many at once*, *what to do when one fails*, and *how to reduce* — not the API of any single framework. Cross-framework: LangGraph `Send`, CrewAI parallel tasks / Flow, `asyncio.gather`, `ThreadPoolExecutor`, LlamaIndex batch retrieval.
# Map-Reduce / Dynamic Fan-Out · SOP > Pattern: `results = reduce(combine, parallel_map(f, L))` where `f` is one > or more LM calls. The *only* reason to fan out is that latency or > throughput matters more than the cost of doing it. The *only* reason to > fan in is that the consumer wants one answer, not N. > Source posture: claims grounded in primary docs and 2026 production > write-ups, cited inline with short tags resolved in the citation index. --- ## 1. 何时激活 (Activation Rules) Activate this skill when **any** of these is true: - The task description contains "for each X, do Y" where Y involves an LM call, a retriever hit, or any I/O-bound step costing >100ms. - The coder is about to write a `for item in items: result = llm(item)` loop and the items are independent (no item depends on the previous result). - The codebase already has `asyncio.gather(...)`, `ThreadPoolExecutor(...)`, `Send(...)`, `Process.hierarchical` parallel branches, or `crew.kickoff_for_each(...)` and the question is *how* to use them safely. - The user mentions any of: "summarize N docs", "rank top-K candidates", "vote across M models", "ensemble", "parallel agents", "multi-query retrieval", "scatter-gather", "fan out". - A LangGraph graph is throwing `InvalidUpdateError` on a key two parallel branches write to (see OP-3 cross-link in skill `O5 state-reducer`). - A LangGraph `Send`-based fan-out is hitting `GRAPH_RECURSION_LIMIT` or rate-limit 429s because all N workers fired at once `[aipractitioner/scaling]`. Do **not** activate when: - N is statically 1 or 2 (just write the calls inline; setup cost ≥ win). - Items depend on each other (sequential reasoning, chain-of-thought across docs) — fan-out destroys the dependency. - The downstream code only needs the *first* successful answer — use `asyncio.wait(..., return_when=FIRST_COMPLETED)`, not gather. - The "fan-out" is into a single batched API call (e.g., embedding 100 strings in one OpenAI request). That's a batched single call, not map-reduce. Use it; it's cheaper. --- ## 2. 核心心智模型 (Core Mental Model) **Fan out for latency, fan in for coherence.** The whole protocol is two questions: *what runs concurrently?* and *how do the answers merge?* Everything else — `Send`, `gather`, `Semaphore`, reducers — is mechanics. Three load-bearing concepts: 1. **The unit of fan-out is the item, not the call.** `f(item)` may itself be a multi-step LM workflow (retrieve → rerank → synthesize) — that is fine. What you parallelise is the *per-item function*. Don't confuse "parallel LM calls" with "parallel workflow instances". The latter is what `Send` and `gather(f(x) for x in L)` actually do `[deepwiki/mapreduce]`. 2. **Concurrency is bounded, never infinite.** Every external dependency (OpenAI/Anthropic API, your vector DB, your KV cache) has at least one of: RPM limit, TPM limit, connection-pool limit, GPU KV-cache budget. `asyncio.gather(*[call(x) for x in 100_items])` will *not* call 100 things — it will fire 100, the API will 429 most, and you'll spend the next 20 minutes in retry-storm hell `[newline/asyncio-llm]` `[tianpan/structured-concurrency]`. The first thing you choose, before any code, is **N_concurrent**. 3. **The reducer determines the shape of the answer.** `concatenate` keeps all evidence (large output, no judgment); `summarize` collapses (information loss, smaller output); `vote / majority` picks one (lossy but decisive); `rank-top-K` selects the best few. Pick the reducer *before* you write the map — because the map's `expected_output` shape is dictated by the reduce. The Pregel-lineage version of the same point: **a fan-out / fan-in is one superstep**. Either it all succeeds and the reduce node runs, or one branch fails and (in LangGraph) the whole superstep is discarded `[aipractitioner/scaling]`. Your code must decide *before* fan-out which semantics you want: atomic-or-nothing, or best-effort-with-holes. --- ## 3. SOP 工作流 (Standard Operating Protocol) Walk this top-down. Each step has a decision gate. ### Step 1 · Confirm items are independent Gate: can `f(item_i)` run without seeing `f(item_j)` for any `j ≠ i`? If **no**, stop. You have a sequential or recursive problem masquerading as map-reduce. Use a chain, or model the dependency explicitly (DAG, beam search, etc.). Fan-out will silently drop the cross-talk. ### Step 2 · Estimate cost honestly Before any code, multiply: ``` cost = N · cost_per_item (in $, tokens, AND seconds_wall) peak_rps = N_concurrent / mean_latency_per_item ``` Three numbers must fit inside three budgets: - `cost_$` ≤ task budget - `peak_rps` ≤ min(API RPM/60, vector-DB QPS, GPU concurrency) - `peak_tps` ≤ API TPM / 60 — TPM is the silent killer: 50 parallel calls each with a 4k-token prompt instantly exceeds most providers' TPM even if RPM is fine `[newline/asyncio-llm]`. If any budget is tight, **fan-out is the wrong tool**. Options: batch calls into single API request (embeddings, reranking), reduce N (pre-filter items), or accept sequential. ### Step 3 · Pick N_concurrent (the most important number in the file) Default rubric: | Constraint | Pick | |---|---| | API-bound (OpenAI/Anthropic) | `min(10, RPM/60 · target_latency_s)` — keep at most one "request-second" of headroom | | Self-hosted vLLM/SGLang | Start at `num_kv_blocks / mean_prompt_blocks`; for most setups 8–32 | | Vector DB retrieval | Provider QPS limit / 2 (leave headroom for other paths) | | Mixed (LM + tool calls) | Constrain by the slowest dependency | | No instrumentation yet | **Start at 5**. Always. Then measure. | `N_concurrent` is enforced with `asyncio.Semaphore(N)` for asyncio, `ThreadPoolExecutor(max_workers=N)` for sync, `max_concurrency` config in LangGraph, `max_rpm` in CrewAI. Never rely on the framework's defaults. ### Step 4 · Decide the failure policy *before* writing the fan-out Four canonical policies — pick one explicitly: | Policy | When to use | How | |---|---|---| | **Abort-all** | One missing item invalidates the answer (legal review, regulated workflows) | `asyncio.gather(*, return_exceptions=False)` — first exception cancels siblings. In LangGraph this is the *default* superstep semantic `[aipractitioner/scaling]`. | | **Best-effort** | Concatenate / summarize use cases — partial is OK | `asyncio.gather(*, return_exceptions=True)` then filter; or per-task `try/except` returning a sentinel | | **Retry-then-skip** | API flakiness is the main failure | Wrap `f` with tenacity / backoff: 3 attempts × exponential, then sentinel | | **Quorum** | Vote / ensemble — need at least K of N | `asyncio.as_completed`, collect K, cancel the rest | Anti-pattern: making this decision implicitly. The single most common production bug in this pattern is "I assumed `gather` would skip failures" or "I assumed one failed branch wouldn't kill the rest" — both are wrong defaults `[aipractitioner/scaling]` `[newline/asyncio-llm]`. ### Step 5 · Pick the reducer | Reducer | Shape change | Cost | When to use | |---|---|---|---| | `concatenate` | `[A, B, C]` → `"A\nB\nC"` | None | Downstream LM can handle big context; you want full evidence | | `summarize` (LM call) | `[A, B, C]` → `"abc"` | +1 LM call | Output must fit in a final prompt; lossy by design | | `vote / majority` | `[A, B, A]` → `A` | None | Self-consistency / ensemble agreement | | `rank-top-K` | `[(a,0.9), (b,0.4), (c,0.7)]` → `[a, c]` | None | Multi-query retrieval, reranking | | `merge-dedupe` | overlapping lists → set | Cheap | Multi-query retrieval, multi-source enrichment | | `tree-reduce` | binary `combine(x, y)` recursively | log₂(N) LM calls | When pairwise merging is meaningful (summary-of-summaries) | Choose by asking: *what does the next node consume?* The reducer is just "adapt the fan-out shape to the consumer's input shape." ### Step 6 · Set per-call timeout *and* total wall-clock budget Two independent timeouts: - **Per-call**: `asyncio.wait_for(call, timeout=30)` — protects against one stuck call holding a semaphore slot forever (the canonical "100 items, 99 done in 5s, the whole job blocked 5 min on item 73"). Default 30s. - **Total wall**: `asyncio.wait_for(gather(...), timeout=N · mean + 3σ)` — protects against pathological N. Default 2× the optimistic wall estimate. In LangGraph, `recursion_limit` is *not* a timeout — set both, plus node-level `RetryPolicy` `[lc-docs/errors]`. In CrewAI, `max_rpm` rate- limits but does not timeout — wrap `kickoff()` in `asyncio.wait_for`. ### Step 7 · Reduce, then verify the reduction is sane After the reducer runs, do one cheap LLM-free check: - `concatenate` → assert `len(merged)` is within expected bounds; raise if one branch returned a 50KB blob. - `vote` → log the vote distribution; if it's near-uniform, the items weren't actually deciding the same question — go back to Step 1. - `rank-top-K` → assert scores are not all identical (failure mode of a broken reranker). These cheap checks catch the "fan-out succeeded but the answer is garbage" bug class that no exception will surface. --- ## 4. 操作模型 (Operation Models) Format: **Trigger → Action → Output → Evidence**. ### OP-1 · `asyncio.gather` with bounded concurrency (Python baseline) - **Trigger**: Pure Python, no graph framework, async LM client (Anthropic / OpenAI), N items, you want a list back. - **Action**: ```python sem = asyncio.Semaphore(N_CONCURRENT) async def bounded(item): async with sem: return await asyncio.wait_for(f(item), timeout=PER_CALL_S) results = await asyncio.gather( *[bounded(i) for i in items], return_exceptions=True, # explicit best-effort ) ok = [r for r in results if not isinstance(r, Exception)] ``` - **Output**: A list (or list-with-holes) ready to reduce. Semaphore is the rate limiter; `return_exceptions=True` is the failure policy; `wait_for` is the per-call timeout. - **Evidence**: `[newline/asyncio-llm]`, `[soumendrak/semaphore]`, `[superfastpython/gather]`. ### OP-2 · `asyncio.as_completed` for quorum / first-K - **Trigger**: You only need K-of-N successful results (vote with early-exit, "first 3 of 10 retrieve hits"). - **Action**: ```python tasks = [asyncio.create_task(f(i)) for i in items] done = [] for fut in asyncio.as_completed(tasks): try: done.append(await fut) except Exception: pass if len(done) >= K: for t in tasks: t.cancel() break ``` - **Output**: First K successful results; remaining cancelled to save cost. - **Evidence**: `[instructor/learn-async]`. ### OP-3 · LangGraph `Send` for dynamic fan-out - **Trigger**: Inside a LangGraph state graph, the number of parallel branches is decided at runtime from state. - **Action**: ```python from langgraph.types import Send def route_fanout(state): return [Send("worker", {"item": x}) for x in state["items"]] graph.add_conditional_edges("planner", route_fanout, ["worker"]) ``` The worker writes to a list-typed state key with a reducer (see OP-4). Cap concurrency at `.compile()` time via `compile(...).with_config({"max_concurrency": N})` or `graph.invoke(..., {"max_concurrency": N})`. - **Output**: True per-invocation map-reduce. Each `Send` is its own trace segment in LangSmith. - **Evidence**: `[deepwiki/mapreduce]`, `[mlplus/langgraph-mr]`, `[medium/send-api]`. Skill `langgraph-sop` OP-4 is the canonical entry. ### OP-4 · Add a reducer for parallel writes (cross-link `O5`) - **Trigger**: Two or more `Send`-spawned workers (or any parallel branches) write to the same state key. Without a reducer, LangGraph raises `InvalidUpdateError`. - **Action**: `summaries: Annotated[list[str], operator.add]` for concatenate, `add_messages` for chat, or a custom reducer for dedupe/top-K. - **Output**: Parallel writes merge per the reducer's algebra. - **Evidence**: `[cheatsheet/gotchas]` "Reducers are mandatory, not optional, for parallel execution"; `[lc-docs/persistence]`. ### OP-5 · CrewAI `kickoff_for_each` (parallel crew invocations) - **Trigger**: Same crew, N independent inputs (e.g., one report per topic). - **Action**: ```python results = crew.kickoff_for_each(inputs=[{"topic": t} for t in topics]) # async variant: kickoff_for_each_async ``` Rate-limit at crew creation: `Crew(..., max_rpm=30)`. Wrap calls in `asyncio.wait_for` for total wall budget. - **Output**: Parallel crew runs, one result per input. - **Evidence**: `[crewai-docs/kickoff]`. Note: CrewAI's hierarchical process is *not* a fan-out primitive — see DC-3. ### OP-6 · `ThreadPoolExecutor` for sync LM clients - **Trigger**: The LM client is synchronous (legacy SDK, no async variant), but the workload is I/O-bound. - **Action**: ```python from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=N_CONCURRENT) as ex: results = list(ex.map(f, items, timeout=PER_CALL_S)) ``` `as_completed(futures, timeout=...)` for failure isolation. Avoid for CPU-bound `f` — use `ProcessPoolExecutor` or a job queue. - **Output**: Bounded-concurrency parallel sync calls. - **Evidence**: Python stdlib `concurrent.futures` docs. ### OP-7 · LlamaIndex multi-query / batch retrieval - **Trigger**: One user question → N rephrased queries → retrieve over each → reduce. - **Action**: `MultiQueryRetriever` or `QueryFusionRetriever` with
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen