The Engineering Boundaries That Make AI Agents Controllable
It's been a while since we talked about Agents. Today, let's continue discussing the key implementation points of Agents. In fact, the main issues with an Agent in daily production are controllability and reliability. So, although theoretically an Agent is just a simple combination of LLM + Tools + RAG, in reality, many fundamental details need to be handled across different scenarios, such as:
- Can it correctly understand the user's goal?
- Why did it choose tool A instead of B?
- When parameters are missing, will it ask for clarification or retry?
- After a tool fails, will it attempt repair or degradation?
- Can actions like deleting, refunding, sending emails, or changing configurations be blocked by permission and confirmation mechanisms?
- How does it resume from a checkpoint after a long task is interrupted?
- Will memory expire, conflict, or become polluted?
- When a system problem occurs, how do you pinpoint whether the issue is with retrieval, planning, parameters, tools, or model generation?
We've actually discussed these issues many times. Basically, a complete Agent includes at least six types of capabilities:
Goal Understanding, Task Planning, Tool Use, State & Memory, Execution Feedback, Security & Evaluation.
The AI model is indeed the key decision-making component within an Agent, but what determines whether the system is controllable and reliable is almost entirely the external engineering boundaries.
For example, suppose you want to build a "Contract Assistant" AI Agent. A user might then propose:
Find the contract the boss sent last week, check the payment terms, summarize them into a paragraph, and prepare it to be sent to finance.
This scenario requires an entire task chain, such as:
- Understanding contextual entities like "last week," "the boss," "that contract"
- Searching emails and identifying candidate attachments
- Reading and parsing the corresponding contract
- Retrieving the payment terms
- Summarizing the terms completely and faithfully to the original content
- Generating a draft to be sent to finance
- Requesting user confirmation before actually sending
- Then entering the email and sending it
So, how do you think this task should be handled appropriately? Generally, the Agent flow for a "Contract Assistant" should look something like this, combining a dynamically planning Agent with fixed-process workflows:
This is actually the most common hybrid implementation in a production environment:
The Workflow determines the executable track, and the Agent is responsible for semantic judgment and dynamic selection within that track.
Then, looking at the previous diagram from both an external and internal perspective, it can be broken down into multiple layers:
- Entry Layer: Identity, tenant, session, request standardization
- Routing Layer: Identify intent, risk, and task type
- Orchestration Layer: Maintain state, budget, checkpoints, and next steps
- Planning Layer: Decompose tasks, select tools, judge missing parameters
- Context Layer: Assemble current state, knowledge, memory, and tool descriptions;
- Execution Layer: Verification, authorization, confirmation, idempotency, invocation, and circuit breaking
- Validation Layer: Judge whether tool results are trustworthy and if the task is complete
- Output Layer: Generate a final result with evidence, explainability, and correct formatting
- Cross-cutting Capabilities: Security, auditing, evaluation, observability, cost governance
Basically, even the most basic Agent will possess these layered capabilities. Because to be reliable and controllable in production, the Agent Loop cannot just be a simple "Think-Act-Observe" loop. In business, it definitely needs:
Understand Goal
→ Read State and Context
→ Generate Candidate Plans
→ Select Constrained Tools
→ Parameter and Permission Validation
→ Request Confirmation if Necessary
→ Execute Tool
→ Normalize Results
→ Verify if Completion Conditions are Met
→ Re-plan if Not Complete, Output if Complete
Then, in this chain, failure can occur at every step, so you need to treat errors as normal branches, ensuring at least a result feedback every time.
1. Intent Recognition
This is a perennial topic. A user's question, like the earlier "Find the contract the boss sent last week, check the payment terms, summarize them into a paragraph, and prepare it to be sent to finance," definitely cannot be sent directly to the model for processing.
The Agent must first perform intent recognition. The goal of intent recognition is to make trade-offs and classifications between accuracy, latency, cost, and risk. Here is a common intent recognition classification scenario:
That is, you need to decide how to categorize based on your business needs. An Agent like the "Contract Assistant" clearly has deterministic rules:
The first layer can use deterministic rules, suitable for handling high-frequency, clear, low-ambiguity commands, such as:
- Log out
- Transfer to human agent
- Regenerate
- Cancel current step
- Open settings
The rule layer must be explicit and streamlined. Its main purpose is to quickly intercept the most definite batch of requests. If there are too many rules, it will turn into a mess and might evolve into an unmaintainable collection of keywords and if-else statements.
The second layer can do context-based routing, because many real-world expressions are hard to understand without context, for example:
- "This one then"
- "Still not working"
- "Change it to 300"
- "Try again"
So this layer needs to route based on scenarios like the previous conversation turn, the current page and selected object, and the current task node. This means using some lightweight local small text models combined with historical information to quickly make semantic intent judgments.
- The third layer is complex task understanding and tool planning. Only tasks with high complexity or those that couldn't be matched will finally enter the large model for processing. Even at this layer, the model's output cannot just be an intent label; we need a structured routing result, for example:
{
"intent": "review_contract_and_prepare_email",
"entities": {
"sender_role": "manager",
"time_range": "last_week",
"document_type": "contract",
"target_audience": "finance"
},
"missing_fields": [],
"risk_level": "medium",
"confidence": 0.87,
"next_action": "search_mail"
}
In intent recognition, there's always a confidence score. Its role is to decide the next step. Generally, the confidence score can trigger at least three different strategies:
- High confidence / Low risk: Route directly
- Medium confidence: Enter a stronger model or supplement context
- Low confidence / High-risk ambiguity: Clarify with the user
For intents like "cancel order" and "cancel appointment," we certainly can't rely on the model to gamble on a 90% success rate. High-risk operations definitely require a human-in-the-loop. Even if matched, they need to enter a clarification stage.
2. Orchestration and State Machine
After the Agent knows the user's intent, it must also know which step it's on. You can't just use the historical conversation to judge this state, because chat logs only have input history and lack a complete task state. Therefore, a reliable Agent State needs to contain at least something like:
from typing import Any, Literal, TypedDict
class AgentState(TypedDict):
task_id: str
user_goal: str
current_step: str
status: Literal["running", "waiting_user", "completed", "failed"]
collected_slots: dict[str, Any]
selected_tools: list[str]
observations: list[dict[str, Any]]
evidence: list[dict[str, Any]]
retry_count: dict[str, int]
token_budget: int
time_budget_ms: int
pending_approval: dict[str, Any] | None
Here, we need to save both raw data and structured results in the state as much as possible to achieve controllability. A markdown output definitely won't work; structured data is needed so that the same facts and state can be reused across different nodes.
For instance, the biggest role of the state is to clarify and define completion conditions. Every task must have explicit completion conditions:
- Are the required fields complete?
- Did the key tools succeed?
- Does the evidence cover the question?
- Does the output pass the Schema?
- Are there any unhandled high-risk actions?
- Have the step, time, token, or cost budgets been exceeded?
For example, the completion conditions for the "Contract Assistant" must include: the contract source has been confirmed, whether the payment terms have original text evidence, the summary and evidence must be consistent, the email draft has been generated, and whether the sending has been executed.
Then, it also needs to support 'checkpoint and persistence' capabilities, because long task processes will inevitably have pauses and resumptions, such as:
- Waiting for the user to supplement an order number
- Waiting for approval
- A third-party interface is temporarily unavailable
- Process restart or node failure
- Continuing to run after manual modification of an intermediate state
Supporting the Agent in saving these checkpoints is also a critical requirement. Generally, at least the following needs to be saved: state version, completed nodes, pending execution nodes, tool result summaries, idempotency keys, and approval information.
Finally, there is the support for 'recovery and execution'. The most important thing here is to 'prevent duplicate execution.' Unlike a Coding Agent, where recompiling and re-scanning is not a big deal, this is definitely not acceptable for something like a "Contract Assistant." When the system recovers from a checkpoint, a node might have already run, but we hadn't waited for the result. Therefore, all operations with side effects must consider idempotency:
- Use idempotency keys for sending emails
- Use conditional updates or upsert for data updates
- Save business request numbers for payments and refunds
- Check if a record already exists before creating it
- Do not execute irreversible side effects before approval
This part is actually not directly related to the Agent; it's more about the business backend's capabilities and state synchronization design.
3. Tool System
Tools are essentially the business abstraction within an Agent and represent the Agent's value and intellectual performance. However, defining a tool isn't just about writing a function call; it also requires a reliable process to support it:
Generally, a tool in an Agent is like a contract. You need to define and prepare a complete set of definitions for the tool, such as:
- Name and version
- Applicable and prohibited scenarios
- Input JSON Schema
- Output Schema
- Permission requirements
- Risk level
- Whether it has side effects
- Whether it needs to support idempotency
- Timeout, rate limiting, and retry strategies
- Possible standard error codes
For example, a tool definition could look like:
from typing import Literal
from pydantic import BaseModel, Field
class SearchMailArgs(BaseModel):
sender_role: Literal["manager", "finance", "customer", "unknown"]
start_date: str = Field(description="ISO-8601 date")
end_date: str = Field(description="ISO-8601 date")
attachment_type: Literal["contract", "invoice", "any"] = "contract"
max_results: int = Field(default=20, ge=1, le=50)
class ToolProposal(BaseModel):
tool_name: Literal["search_mail", "read_attachment", "extract_clause", "create_draft"]
arguments: dict
expected_result: str
risk_level: Literal["read", "write", "high_risk"]
Of course, here the Schema only guarantees the structure of the execution result, but whether the result is correct still requires subsequent judgment. For example: a date might be in a legal format but out of range, an amount might be a number but exceed permissions, an email might be valid but belong to the wrong recipient. These all require business validation after the Schema check.
Next is tool matching and retrieval. The intent decomposition we discussed earlier is largely aimed at matching the right tools. Therefore, tools should be as precise as possible. If the model's capability isn't top-tier and rules can't achieve fine-grained tool distinction, then the number of tools must not be too large.
For instance, exposing dozens or hundreds of tools to the model at once mostly just increases the chance of misselection and parameter confusion. So, generally, an Agent will first retrieve tools and then register them for the large model in this task. For example, you can:
- Filter the tool set based on intent and domain
- Remove unavailable tools based on user permissions
- Exclude inappropriate tools based on the current state
- Only expose the most relevant small set of tools to the model
Then, before execution, format validation, business validation, permission validation, and risk confirmation are all required. This is also the basic etiquette of tool execution.
def execute_tool(proposal, context):
args = schema_registry.validate(proposal.tool_name, proposal.arguments)
policy.authorize(context.user, proposal.tool_name, args)
policy.validate_business_rules(context, proposal.tool_name, args)
if policy.requires_confirmation(proposal.tool_name, args):
return pause_for_human_approval(proposal, args)
idempotency_key = build_idempotency_key(
context.task_id, proposal.tool_name, args
)
return executor.run(
tool_name=proposal.tool_name,
args=args,
idempotency_key=idempotency_key,
timeout_ms=10_000,
)
Additionally, just like the intent confidence and risk mentioned earlier, tools also need a risk rating. Or rather, the intent risk mentioned before basically comes from the tool risk. For example:
| Level | Typical Operations | Default Strategy |
|---|---|---|
| R0 Pure Reasoning | Classification, summarization, rewriting | Can execute automatically |
| R1 Read-only | Query orders, search emails, read documents | Auto-execute after authorization, log audit |
| R2 Reversible Write | Create drafts, add tags, save temporary configs | Can auto or light confirm, must be idempotent |
| R3 High-risk Write | Send emails, refund, delete, transfer, execute scripts, modify production configs | Strong confirmation, least privilege, full audit |
| R4 Forbidden or Isolated | Exceeding business authorization, cross-tenant access, sensitive batch operations | Directly reject or transfer to human |
Even on the user UI, we might need to display things like: what tool will be called next, which object it will operate on, the scope of impact, whether it's reversible, and what the key parameters are.
Then, tools will inevitably fail during execution. The failure errors here must also be structured, for example:
{
"status": "error",
"error_type": "MISSING_ARGUMENT",
"retryable": false,
"field": "contract_id",
"message": "Missing contract identifier, requires user to select a candidate contract",
"suggested_action": "ask_user"
}
Common error classifications are generally:
- Node errors: Timeout, rate limiting, network jitter; can retry with exponential backoff
- Model-repairable errors: Parameter format errors, missing fields; can re-plan after feedback
- User-repairable errors: Missing order number, non-unique candidate objects; should ask the user
- Permission and policy errors: Non-retryable, should explain and terminate
- Permanent business errors: Order status does not allow cancellation; should not call repeatedly
- Systemic errors: Tools fail consecutively; trigger circuit breaking and alerts
With error classification and rating, you can then retry errors. The number of retries can also be configured based on the tool's idempotency, error type, latency budget, and business consequences.
High-risk operations definitely allow zero retries.
4. Structured Output
We've been emphasizing that structured data needs to flow and be held internally within the Agent. However, getting structured output from a model also has its nuances. Common problems when an LLM outputs JSON include:
Interleaved explanatory text, Markdown code blocks, missing fields, type errors, truncation, out-of-bounds enums, and semantic errors.
These are all very common problems. Therefore, compatibility and automatic error handling require a reliable JSON flow to support, such as:
Explicit Output Contract
→ Model-native Structured Output / Function Calling
→ Check stop reason and truncation
→ JSON Parsing
→ Schema Validation
→ Business Semantic Validation
→ Limited Repair
→ Retry with Error Information
→ Degradation or Task Splitting
Generally, it's definitely preferable to use the model's native structural constraints first. For example, for models that support strict JSON Schema, Function Calling, or Structured Output, you certainly cannot rely on prompting with "only output JSON," which is a completely unreliable promise.
But generally, we must still distinguish three states of correctness:
- Syntactically correct: JSON can be parsed
- Structurally correct: Fields and types conform to the Schema
- Semantically correct: Values conform to real business and user intent
Strict Schema validation mainly solves the first two; semantic correctness requires subsequent business review.
Then, for syntax and structural correctness issues, errors will definitely occur during Agent operation. So, we can add multiple methods for repair and retry, but don't guarantee a fix can always be made, otherwise it's easy to get into an infinite loop.
In fact, for JSON structural problems, the ones that can be safely repaired are usually only clear formatting issues, such as:
- Removing code blocks
- Extracting the unique JSON fragment
- Removing trailing commas
- Completing determinable closing brackets
- Cleaning up irrelevant text before and after
But if you encounter these problems, repair is generally not recommended, for example:
- Not knowing which enum the model originally intended to choose
- Field values that are contradictory in business terms
- Output is truncated and missing a large amount of content
- Multiple candidate JSONs where it's impossible to judge which is correct
In these cases, you should directly judge it as a failure and then re-request or split the task for a retry, directly ignoring this JSON result.
Additionally, it's not recommended to use an Agent to process long and deeply nested structured data. If the data hierarchy is long and deep, it needs to be split. For example, you can break it down into:
- First extract facts
- Then generate tags
- Then make risk judgments
- Finally, merge them programmatically
Otherwise, the probability of truncation and problems will be very high. After splitting, each sub-step can also be independently validated and evaluated.
5. RAG
RAG is also a perennial topic. According to common rules for Agents, it can generally be divided into:
- First, ensure it can be found
- Second, ensure it's ranked correctly
- Finally, ensure the answer is faithful
That is to say, although we might simply say RAG is just "Vector DB + Top K + LLM," in an actual production chain, it generally includes at least:
Parsing, Chunking, Indexing, Query Understanding, Multi-path Recall, Deduplication, Reranking, Context Assembly, Generation, and Citation.
For example, document parsing: a contract is a very typical scenario. It needs to preserve the original structure, because while ordinary text can be quickly chunked by headings, paragraphs, and length, contracts, technical specifications, policy documents, tables, and scanned copies require more fine-grained structured parsing.
So for documents, a Chunk needs to save more than just the body text:
- Document ID, version, and source
- Heading hierarchy
- Page number and paragraph position
- Effective date
- Permission tags
- Content type
- Relationship with preceding and succeeding Chunks
Then, layer resources based on business needs, for example, "70% ordinary documents, 30% high-value documents." This needs to be experimented with based on your actual business situation.
Then, the next two things to do are: Recall and Reranking.
In the recall phase, you can generally do addition, mainly to avoid missing key evidence. For example, you can combine:
- Vector Retrieval: Handles semantic similarity
- BM25: Handles keywords, numbers, proper nouns
- Metadata Filter: Handles time, department, document type, and permissions
- Query Rewrite: Supplements entities, synonymous expressions, and time ranges
- Multi-Query: Expands recall from multiple angles
- Parent-Child Retrieval: Small chunks for matching, large chunks for providing context
Then, the reranking phase needs to do subtraction. After recalling many candidates, we need to use a Reranker and rule-based features for sorting, such as:
- Direct relevance to the question
- Title and chapter matching
- Source authority
- Timeliness
- Permissions and tenant
- Whether it contains multiple facts required for a complete answer
Finally, there is context assembly. Once everything is retrieved, you can assemble the context state. Here, some secondary optimizations can generally be done, such as:
- Controlling the maximum possible length for each source
- Removing duplicate content
- Evidence coverage for multi-hop questions
- Handling conflicts between old and new versions
- Proofreading citation numbers
- Isolating untrustworthy content from system instructions
For example, for a contract assistant, the retrieval results must be traceable back to a specific contract, page, and clause. You cannot just give the model a piece of text that has lost its source.
6. Memory
Memory in an Agent can generally be divided into short-term state, long-term facts, and external knowledge. Structurally, it's similar to:
That is, generally, you can have four layers of memory, for example:
| Layer | Content Saved | Typical Storage | Lifecycle |
|---|---|---|---|
| Working Memory | Current task steps, temporary variables, recent messages | Graph State / Checkpoint | Current thread |
| Summary Memory | Project goals, key decisions, unfinished items | Summary table or thread summary | Medium-term |
| Semantic Memory | Historical conversation snippets, code discussions, events | Vector DB | Long-term, retrievable |
| Structured Memory | Stable preferences, project configs, contacts, versions | Relational DB / KV / Document DB | Long-term, updatable |
Simply put:
- Short-term state solves "what step am I on now"
- Long-term memory solves "what past information is relevant to the current task"
- External knowledge bases solve "what factual materials does the organization possess"
These things need to be stored separately and cannot be mixed into a single vector database.
In reality, memory writing also needs a strategy. From a user's perspective, not every sentence is worth long-term preservation. So, before writing, at least judge:
- Is this long-term, stable information?
- Was it explicitly saved by the user?
- Does it contain sensitive information?
- Will it duplicate or conflict with existing memories?
- Is the content just a temporary hypothesis?
- Does it have an expiration time?
- Does the user need to view, modify, or delete it?
For example, after a project switches from Flutter to Swift, old memories can no longer be recalled under the "current tech stack." Therefore, based on the business form, we need to maintain version relationships: used Flutter in the past, a decision was made on a certain day to switch to Swift, and the current valid value is Swift.
Finally, it's also necessary to prevent memory pollution, such as content read from web pages, emails, or tool results that might contain malicious instructions.
Therefore, external content can only serve as data and cannot be automatically upgraded to system instructions or long-term preferences. It is generally recommended to save structured information for memories, such as:
Source, creation time, confirmation status, credibility, validity period, override relationships, and sensitivity level. Important facts are best written after programmatic verification or user confirmation.
For this part, you can also look at the introduction in "Claude Opus 5 System Prompt Leak". Claude designed a set of persistent memory filesystem specifications within its system prompt.
7. Evaluation
Evaluation is also a key implementation. In Agent development, the entire evaluation mainly looks at the execution trajectory:
Generally, a relatively complete evaluation can be divided into four levels, for example:
Level 1: Component Evaluation, testing individually:
- Intent recognition accuracy and clarification rate
- Tool Top-K hit rate
- Parameter field accuracy
- JSON / Schema success rate
- Retrieval Hit Rate, Recall@K, MRR, NDCG
- Reranker ranking effectiveness
- Permission and risk classification recall rate
Level 2: Trajectory Evaluation. Even if the final answer might be correct, you also need to pay attention to whether the process was safe. Trajectory evaluation can focus on:
- Were the necessary steps chosen?
- Were any redundant tools called?
- Was a write-then-confirm pattern followed?
- Were parameters hallucinated when missing?
- Did any loops occur?
- Was the budget exceeded?
- Did execution access unauthorized resources?
- Was the recovery path after failure correct?
Level 3: Result Evaluation, focusing on things like:
- Task completion rate
- Factual correctness
- Citation consistency
- Format correctness
- Whether user constraints were met
Level 4: Business and Operational Metrics, such as:
- Single-task success rate
- Human handover rate
- Number of user clarification rounds
- P50 / P95 / P99 latency
- Token, model, and tool cost per task
- High-risk operation interception rate
- Retry rate and circuit-breaker trip rate
- User satisfaction and first-contact resolution rate
So, building an Agent might be fast, but running evaluations, testing stability, reliability, and model compatibility is actually the most time-consuming and labor-intensive process.
Then, the evaluation for RAG must also be able to trace back to the fault layer. Because in a "Contract Assistant," RAG problems are the biggest and most likely to occur. So, you need to know the trajectory of RAG problems. Trace information must support tracing back to the fault layer, for example:
| Phenomenon | More Likely Problem | Priority Investigation |
|---|---|---|
| Low Context Recall | Key materials not retrieved | Chunk, Query Rewrite, Hybrid Search, Top-K |
| Low Context Precision | Too much noise | Reranker, Metadata, Deduplication, Query Understanding |
| High Recall but Low Faithfulness | Materials found, model still hallucinating | Generation Model, Prompt, Citation Constraints, Context Conflict |
| High Faithfulness but Low Answer Relevancy | Not hallucinating but answering the wrong question | Intent, Question Rewriting, Prompt, and Completion Conditions |
| Tool call succeeds but task fails | Tool result did not solve user goal | Result Validation and Re-planning |
| High offline score, low online satisfaction | Data distribution or business goal mismatch | Online Samples, Task Definition, Product Interaction |
The most important thing is to establish a complete Agent test set.
8. Observability Trace
Trace is the most important entry point for AI debugging of an Agent. We talked a lot about trajectories earlier, and that refers to Trace. An Agent must record a complete trajectory.
For example, record for each run:
trace_id / task_id / thread_id
User and tenant context
Model, version, temperature, token usage
Routing result, confidence, and risk level
State snapshots and node latency
Recall Query, candidate Chunks, reranking scores, and citations
Tool candidates, final selection, parameters, and Schema version
Authorization, confirmation, and policy decisions
Tool latency, status codes, retries, and idempotency keys
Final completion conditions and failure reasons
Manual modifications and user feedback
Of course, in logging, we cannot save Prompts, email bodies, and sensitive parameters without boundaries. Trace itself must also adhere to minimization, desensitization, access control, and retention periods. For example, key dashboards can include:
- Task success rate
- Distribution of failure reasons
- Latency per node
- Tool error rate
- Average number of steps and loop rate
- Cost per task
- Human confirmation rate and rejection rate
- RAG recall and citation quality
- Comparison of different models, Prompts, and tool versions
In short, the Trace must be complete. As for whether desensitization is needed, that actually depends on your business.
Conclusion
So, these are all the basic Agent logical concepts, and also an introduction to the Agent capabilities needed for a "Contract Assistant." Whether you build it the old-fashioned way or with AI, you need to plan holistically on a similar foundation. The core is how to build a Flow and a golden test framework based on your business.
The quality and effectiveness of an Agent must have a set of quantifiable metrics. The entire goal and function of an Agent is to make AI reliable and controllable. In a coding Agent, a hallucination might not be a big problem, but in contract, legal, or shopping scenarios, a single hallucination or escaped operation can lead to a huge P0 issue.