跪拜 Guibai
← All articles
Frontend · Flutter · Android

Building a Production Flutter Agent with GenKit: State Machines, Tool Safety, and Audit Loops

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Most Agent demos stop at a single model call. A real enterprise Agent needs tool permission boundaries, state-machine-enforced output consistency, and an audit loop that catches hallucinations without rewriting model answers. This implementation shows how to get those guarantees on Flutter with Google's own SDK, and the patterns for tool safety, structured recovery, and trace-driven debugging apply to any stack.

Summary

A production Flutter Agent built with Google's GenKit SDK moves beyond simple API calls by wrapping every turn in a typed Flow with 21 lifecycle steps. The system defines tool boundaries through a required/forbidden/candidate set so the model never sees tools it shouldn't use, and a trust-by-side-effect matrix gates execution of mutating or external actions. An 11-state fine-grained state machine tags every transition with its actor (FSM, LLM, or GenKit) so trace failures immediately identify whether the model or the engineering is at fault.

Retrieval-augmented generation runs across four decoupled grounding pathways and five retrieval modes fused with Reciprocal Rank Fusion, while a query-rewrite layer produces five variant types to catch terminology mismatches. The Agent Loop itself delegates tool-calling cycles entirely to GenKit's `generateStream`, with three structured recovery paths for missing tools, context overflow, and truncated answers. A dedicated Auditor extracts factual claims from the model's answer, grounds each against evidence, and triggers regeneration with specific issue codes rather than rewriting answers.

Every cross-boundary data structure carries a versioned schema, prompts are i18n-ready Dotprompt templates with dual character-and-token budget trimming, and a full trace-and-replay harness packages turn dependencies for offline reproduction. The result is an enterprise Agent where semantic decisions stay with the model but all engineering invariants are enforced by the runtime.

Takeaways
GenKit's Flow wraps every Agent turn into a typed, traceable function with 21 lifecycle steps, not a loose chain of prompts.
Tool permissions use a three-set model (required, forbidden, candidate) so the LLM never sees tools it shouldn't call, eliminating guesswork at the prompt level.
An 11-state FSM tags every transition with its actor (FSM, LLM, or GenKit), making it immediately clear in traces whether a failure is a model or engineering problem.
Mutable buffers like answer and thinking are append-only inside a controller; only at the commitTurn state does a committer push the final session object to the UI.
The Agent Loop does not use a hand-written while(needsTool) loop. Tool cycling is delegated entirely to GenKit's generateStream(maxTurns:) to avoid reimplementing parsing, backfill, and cancellation.
Three structured recovery paths handle missing tool calls (force retry with toolChoice:required), context overflow (incremental prompt compaction that never trims the user's current turn), and truncated answers (continuation or re-synthesis from tool results).
Side-effect-free required tools are pre-run before the Agent Loop starts, stuffing results into the prompt as completed tool_calls to save latency and give the model grounding facts on the first generation.
RAG uses four independent grounding pathways and five retrieval modes fused with Reciprocal Rank Fusion; a query-rewrite layer generates five variant types to bridge terminology gaps like 'lag' vs 'latency'.
A Source Diversity Reranker caps chunks per source document so the model sees different angles rather than three near-identical chunks.
The Auditor extracts factual claims from the answer, grounds each against evidence, and returns structured IssueCodes. It never rewrites the answer, preserving full traceability of model capability.
Every cross-boundary data structure carries a versioned schema name (e.g., local_agent.trace_v1) so old data isn't misread when schemas evolve.
Prompt templates are i18n-ready Dotprompt files with YAML frontmatter; the template body contains only variable injection, while all natural-language strings are injected from Dart via locale-aware message interfaces.
Data masking runs a three-layer strategy: key-based field matching, text-based regex chains for implicit secrets, and a 1200-character length cap on any single string.
A replay harness packages all turn dependencies so any user-reported wrong answer can be reproduced offline on a developer's machine.
Conclusions

Delegating the tool-call loop entirely to GenKit's generateStream rather than writing a custom while loop is a deliberate architectural choice that avoids reimplementing parsing, backfill, and cancellation logic, keeping the Dart side focused on boundary enforcement.

The three-set tool permission model (required/forbidden/candidate) is more robust than prompt-based instructions because the model simply cannot see forbidden tools in its schema, eliminating an entire class of prompt-injection and privacy-bypass risks.

Tagging every FSM state transition with its actor (FSM, LLM, or GenKit) turns trace debugging from guesswork into a clear attribution of whether the model or the engineering failed, which is critical for regression in multi-agent systems.

Pre-running side-effect-free tools before the Agent Loop starts is a latency optimization that also improves answer quality by giving the model grounding facts on its first generation pass, reducing multi-turn back-and-forth.

The Auditor's refusal to rewrite model answers, even when it detects factual errors, is a principled boundary that keeps the trace a pure reflection of model capability. This makes evaluation results trustworthy and prevents local code from silently papering over model weaknesses.

Query Rewrite generating five variant types (original, keyword, technical, code/config, synonym) and fusing them with RRF is a practical answer to the terminology-mismatch problem that single-query BM25 cannot solve, especially for Chinese-English mixed corpora.

Versioning every cross-boundary data structure with a schema name like 'local_agent.trace_v1' is a low-cost insurance policy against breaking changes that most Agent projects overlook until they need to replay old traces.

Keeping Dotprompt templates free of natural-language strings and injecting all text via locale-aware Dart interfaces forces i18n decoupling and prevents the common anti-pattern of hard-coded prompts that break when switching languages.

Concepts & terms
GenKit Flow
Google's GenKit SDK abstraction that wraps a piece of AI business logic into a typed, traceable, debuggable, and deployable function. Unlike LangChain's Chain, a Flow is the central unit of orchestration in GenKit, with built-in streaming, schema validation, and Developer UI support.
Turn Plan (Three-Set Tool Model)
A permission model that defines tool access for a single Agent turn using three sets: required (must call at least one), forbidden (model cannot see these tools at all), and candidate (available for the model to choose). This eliminates prompt-level guessing about permissions and privacy.
Reciprocal Rank Fusion (RRF)
A method for combining search results from multiple queries or retrievers by scoring each document as the sum of 1/(rank + k) across all result lists. The implementation here uses a hybrid of the original similarity score plus the reciprocal rank to avoid losing semantic information.
Claim Extraction & Grounding
An audit technique where a second model call extracts every factual claim from the Agent's answer, then attempts to locate each claim in the provided evidence. Any claim that cannot be grounded produces a structured failure code, triggering regeneration with specific feedback rather than a generic retry.
Dotprompt
GenKit's prompt template format using YAML frontmatter for schema, model, and config declarations, with a template body for variable injection. Templates contain no hard-coded natural language; all strings are injected at runtime via locale-aware interfaces, enabling i18n and version-controlled prompt assets.
Prefill Mechanism
A latency optimization where side-effect-free required tools (like knowledge_search or current_time) are executed before the Agent Loop starts, and their results are inserted into the prompt as already-completed tool_calls. This gives the model grounding facts on its first generation pass and reduces internal loop rounds.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗