AIOps Agent 与 LangGraph
拆解一个生产化 Chat Agent:基于 LangChain 的受控 ReAct 架构
拆解对话入口中的受控 ReAct、工具权限、状态管理与安全收敛。
用户在同一个对话框里,可能连续提出三类完全不同的请求:
“什么是数据库死锁?”
“查看
incident_xxx的状态。”“根据
diagnostic_xxx的报告执行恢复。”
如果只做一个 Demo,这三句话都可以交给同一个大模型,再把知识库、日志查询、诊断和恢复工具一次性全部注册进去。但进入真实项目后,问题很快就会出现:查询状态为什么还要让模型自由规划?恢复请求为什么能绕过人工确认?工具越来越多后,模型为什么开始选错工具?网络中断后,同一条请求会不会重复执行?
这也是我在实现 Chat Agent 时最先遇到的架构问题:开放式对话需要模型具备动态决策能力,但生产系统不能把路由、权限和副作用一起交给模型。
最终我采用的并不是“所有请求都走 ReAct”,而是一套混合架构:
用户请求
→ 输入安全检查
→ Intent Router
→ Execution Policy
├─ direct_read:确定性查询,绕过 ReAct
├─ confirmation_required:高风险动作,先生成待确认操作
└─ bounded_react:开放式任务,进入有预算的 ReAct
→ SSE 公共事件
→ PostgreSQL Run / Event / Audit
本文的中心结论是:
ReAct 负责开放式任务中的动态“思考—行动—观察”循环,LangChain 负责复用模型、消息与工具编排;真正让 Chat Agent 具备生产边界的,是项目自己实现的意图路由、执行策略、最小工具集、预算、人工确认、持久任务和审计。
一、Chat Agent 真正要解决的是什么问题
很多 Chat Agent 的第一版都从下面的循环开始:
User Message → LLM → Tool → Observation → LLM → Answer
这条链路可以回答问题,也可以展示 Tool Calling,但它默认了几个并不成立的前提:
- 每个请求都需要模型参与;
- 模型可以看到所有工具;
- 模型生成的工具参数都可信;
- 一次 HTTP 连接可以覆盖完整执行周期;
- 失败后重新执行不会产生副作用;
- 给用户展示模型输出,就等于完成了审计。
在当前项目中,Chat Agent 不只负责普通对话,还承担了知识问答与 AIOps 系统入口的职责。公开意图被收敛为六类:普通聊天、知识问答、事故查询、启动诊断、诊断状态查询和恢复请求。
这六类请求的风险和执行方式并不相同。知识问答需要动态决定何时检索,事故查询只需要读取已有结构化数据,启动诊断会创建后台任务,而恢复请求必须受人工审批边界约束。
因此,核心问题不是“怎样让模型调用更多工具”,而是:
怎样只在需要模型决策时使用 ReAct,同时让每一次模型调用和工具调用都处于系统可验证的边界内?
二、为什么开放式对话适合 ReAct
固定工作流适合步骤已知、依赖明确的任务。例如“读取诊断报告 → 检查恢复权限 → 生成人工审批请求”,节点顺序在运行前就能确定。
普通对话不同。用户可能问时间,也可能询问某项知识;一次知识问答可能无需检索,也可能需要先加载指定 Skill,再调用 RAG;拿到检索结果后,模型还要判断证据是否足以回答。这些分支很难提前穷举。
ReAct 的价值就在这里。它不要求系统预先写死每一步,而是允许模型在一个短循环中交替执行:
理解当前任务
→ 选择一个被授权的工具
→ 获得公开 Observation
→ 判断是否继续调用工具
→ 生成最终回答
这里的“Reasoning”不等于向用户公开隐藏思维过程。生产系统真正需要保存的是可审计的外部事实:路由结果、工具名称、经过脱敏的参数、公开 Observation、引用和最终结果。模型内部的隐式推理既不应该作为权限依据,也不需要进入 SSE 或审计记录。
ReAct 解决的是运行时路径不确定的问题,但它并不自动解决安全、持久化和权限问题。因此,我采用的是 bounded ReAct:模型可以动态选择路径,但只能在代码预先定义的工具集合、调用次数和时间上限内行动。
三、为什么选择 LangChain,而不是自己手写 Agent 循环
一个最小 ReAct 循环并不难手写:请求模型、解析 tool call、执行函数、把结果作为 Tool Message 追加回上下文,再次请求模型即可。困难在于,当它进入真实项目后,还要处理模型消息格式、结构化工具 Schema、异步执行、事件流、调用限制、中间件和供应商兼容。
LangChain 的价值不是替项目做所有架构决策,而是提供一个稳定的模型—消息—工具编排层。当前实现使用 create_agent 创建 Agent,并通过 astream_events 获取模型和工具的运行事件。
下面是与项目实现一致的简化代码:
tools = [
registry[name]
for name in request.tool_names
if name in registry
]
agent = create_agent(
model=llm_provider.create_chat_model(),
tools=tools,
system_prompt=request.system_prompt,
middleware=build_agent_middleware(request.execution_budget),
)
async for event in agent.astream_events(
{"messages": messages},
version="v2",
):
yield to_public_chat_event(event)
这个选择减少了编排层的重复实现,但边界必须说清楚:
| 能力 | LangChain 负责 | 项目负责 |
|---|---|---|
| 模型接入 | 统一 Chat Model 调用接口 | 模型配置、供应商参数与失败策略 |
| 工具调用 | Tool Schema、Tool Message、Agent 循环 | 工具注册、权限、参数注入和审计 |
| 消息编排 | 模型可消费的消息结构 | 会话归属、记忆筛选与上下文预算 |
| 运行控制 | Agent middleware 和事件接口 | 总调用预算、全局 deadline、重试语义 |
| 流式输出 | 提供运行事件 | 映射成稳定的 SSE 公共契约 |
| 持久化 | 不作为本项目的业务真相来源 | PostgreSQL Run、Event、Message、Audit |
| 高风险动作 | 不替业务判断 | 人工确认、幂等与 fail-closed |
所以,选择 LangChain 不是为了少写所有代码,而是为了把精力放在真正属于业务的控制面上。
四、为什么第一步不是调用模型,而是 Intent Router
如果模型一开始就拿到全部工具,它实际上同时承担了两项职责:理解用户想做什么,以及决定自己拥有什么权限。这两项职责不能混在一起。
当前 Intent Router 先执行输入安全检查,再处理显式资源标识。例如,请求中包含合法的 incident_* 或 diagnostic_* 标识时,系统优先使用确定性规则;无法通过规则判断的普通自然语言,再交给一个受限的 LLM 分类器。
分类器只能返回固定 JSON 字段:
{
"intent": "knowledge_question",
"confidence": 0.86,
"incidentId": null,
"diagnosticTaskId": null,
"needsClarification": false
}
系统会重新校验意图枚举、置信度范围和资源 ID 格式。置信度低于阈值、需要目标却没有提取到目标,都会进入澄清状态。分类调用失败时则安全回退到普通对话,而不是凭空授予诊断或恢复能力。
路由结果只保存公开且可审计的字段:intent、confidence、source、资源 ID、是否需要澄清以及阻断原因,不保存也不依赖模型的隐藏推理。
这一层的意义是:模型可以参与分类,但权限只由经过验证的分类结果派生。
五、为什么 Router 后面还需要 Execution Policy
知道用户意图,并不等于知道应该怎样执行。Execution Policy 把一个已经验证的 Route 编译成不可变策略,核心字段包括:
- 执行模式;
- 当前任务允许和要求的工具;
- 最大模型调用数与工具调用数;
- 整体 deadline;
- 执行成功必须满足的 postcondition。
它最终把请求分成三条路径。
1. direct_read:能确定性完成,就不启动 ReAct
“查看当前事故”或“读取某个诊断报告”已经具有明确目标和固定数据源。让模型先选择 get_diagnostic_report,不仅增加一次模型调用,还可能选错工具或遗漏关键安全字段。
因此,这类请求直接调用 owner-scoped 的 Bridge Service,得到结构化结果,再验证 postcondition。例如诊断报告必须包含恢复模式、是否允许执行、是否需要人工审批、Validator 状态和证据 ID 等字段。可选的 LLM 解释失败时,结构化结果仍然成立,系统只把状态标记为“解释降级”。
这体现了一个很实用的原则:
事实读取是主结果,语言润色只是可降级的视图。
2. confirmation_required:先生成待确认动作,不直接执行
启动诊断和恢复请求会改变外部状态。当前链路不会让 ReAct 在一次对话中直接完成它们,而是先创建 Pending Action,把操作目标、原因和关联 Run 固化下来,再向前端发送 confirmation.required。
用户确认后,系统才能进入后续业务流程。尤其是恢复请求,Chat Agent 创建的是审批或恢复意图,而不是直接执行重启、终止连接等动作。
3. bounded_react:只给开放式任务动态决策权
普通聊天和知识问答进入 ReAct,但预算并不相同。知识问答允许一次 Query Rewrite,并要求知识检索工具可用;普通聊天只需要更小的模型和工具预算。澄清状态下,创建诊断和恢复审批等工具会被移出允许集合。
简化后的策略可以表达为:
if intent == "diagnostic_status":
return direct_read(postcondition="diagnostic_result")
if intent == "recovery_request":
return confirmation_required(capability="recovery_approval")
if intent == "knowledge_question":
return bounded_react(
required_tools={"knowledge_retrieval"},
max_model_calls=3,
max_tool_calls=2,
max_query_rewrite_calls=1,
deadline_seconds=120,
)
这一步让“是否使用 ReAct”从模型偏好变成了系统策略。
六、工具都在本地,为什么模型仍然只能看到一部分
工具注册表回答的是“系统里有什么”,而当前请求的工具目录回答的是“模型现在能看见什么”。两者不是同一个概念。
当前工具来源包括本地工具、知识检索、渐进式 Skill 加载、AIOps Bridge,以及用户启用的 MCP 工具。Runner 不会把 Registry 全量传给模型,而是根据已经验证的意图生成允许列表,再与运行时实际存在的工具取交集。
例如:
| 意图 | 模型可见的典型工具 |
|---|---|
| 普通聊天 | 当前时间、加载 Skill |
| 知识问答 | 知识检索、加载 Skill |
| 事故查询 | 事故列表、事故详情 |
| 诊断状态 | 状态、报告、证据查询 |
| 恢复请求 | 状态、报告、创建审批请求 |
工具的 owner 身份不会作为模型可填写的参数暴露。AIOps Bridge 在构造工具时,从认证上下文闭包注入 owner_user_id,模型只能提供诸如 task_id、limit 等业务参数。这样可以避免模型通过改写 owner 字段访问其他用户的数据。
项目中还实现了独立的 ToolCatalog.compile():它可以检查必需工具是否缺失、生成稳定目录版本,并对缺失能力显式失败。
这是我认为架构拆解中不能回避的一点:有了正确抽象,不代表生产路径已经完整使用了它。
七、怎样防止 ReAct 无限循环或重复调用
受控 ReAct 至少需要三层预算。
第一层是模型调用上限。第二层是工具调用上限。第三层是覆盖整个异步事件流的单调时钟 deadline,而不是每收到一个事件就重新计时。
当前实现使用 LangChain 的 Model/Tool Call Limit Middleware,再增加一个请求级重复调用保护:工具名和规范化参数完全相同时,第二次调用会被阻断。
fingerprint = canonical_json({
"name": tool_call["name"],
"args": tool_call.get("args", {}),
})
if fingerprint in seen:
raise RepeatedToolCallError()
seen.add(fingerprint)
这不是说同一个工具永远不能调用两次。不同时间窗口、不同资源 ID 或不同查询参数仍会得到不同指纹。它阻止的是模型在没有获得新信息时,对完全相同的动作原地打转。
对于可能跨网络重试的 Bridge 工具,仅靠进程内 seen 还不够。系统还会基于 Chat Run、逻辑步骤、工具名和规范化参数生成稳定 tool_call_key,通过持久化 claim、lease 和结果复用实现幂等。只读调用失败后可以安全重试;有副作用的调用如果结果未知,则进入 uncertain,不能盲目补执行。
八、当前上下文是怎样装配出来的
当对话变长后,一个常见现象是:模型明明拥有更长的上下文窗口,回答反而开始忽略最近要求、混淆旧结论,甚至把历史摘要中的一句话当成新指令。
原因是“上下文窗口很大”和“上下文组织正确”是两件事。如果把系统 Prompt、全部工具 Schema、所有 Skill 正文、完整历史、记忆和工具结果直接拼接起来,模型面对的不是更完整的信息,而是一份没有优先级和信任边界的材料堆。
当前实现因此没有直接执行 history + new_message,而是在调用 LangChain Agent 前,先通过 ChatMemoryService 和 ContextEnvelopeService 构造一个有边界的 Context Envelope:
持久化会话与历史消息
|
v
找出尚未压缩的消息 + 本轮候选消息
|
v
估算上下文占用,必要时压缩旧消息
|
v
Context Envelope
|- System Prompt
|- 当前允许的工具描述
|- 带来源的结构化记忆
|- 最近 6 个完整对话轮
|- 有预算的公开 Observation
`- 为模型输出预留的 Token
|
v
LangChain create_agent
1. 上下文由哪些部分组成
ContextEnvelope 不只返回消息,还会记录各部分的预算:
| 上下文组成 | 当前来源 | 为什么需要单独计算 |
|---|---|---|
| System Prompt | 默认约束、用户选择的 Prompt、Skill 目录 | 权限和行为约束不能被普通消息挤掉 |
| Tool Schema | 当前意图允许的工具描述 | 工具越多,Schema 本身占用越高 |
| Structured Memory | PostgreSQL 中的会话结构化记忆 | 保存跨轮目标、事实、决策和资源引用 |
| Recent Turns | 最近 6 个完整 user/assistant 轮次 | 保留当前对话连续性,避免从半轮中间截断 |
| Observations | 已裁剪的公开工具结果摘要 | 为本轮事实证据提供空间,但不能无限增长 |
| Output Reserve | 至少按窗口的约 10% 预留 | 防止输入刚好塞满窗口,模型没有空间回答 |
这里最重要的不是某个固定比例,而是每一类内容都有独立身份。系统能够知道空间究竟消耗在 Prompt、工具、记忆还是历史消息上,而不是只得到一个总 Token 数。
2. 一条新消息进入后发生了什么
用户消息到达后,系统先读取当前用户所属的 Session 和消息历史,并根据 memory_through_message_id 找出尚未进入结构化记忆的部分。随后创建一条只用于估算的 candidate message;在预算通过以前,这条候选消息不会因为上下文超限而被提前写入正常执行链。
准备过程可以简化为:
uncompacted = uncompacted_history(history, session)
candidate_messages = [*uncompacted, candidate_user_message]
if usage(candidate_messages) >= synchronous_threshold:
compact_old_messages_with_cas()
envelope = ContextEnvelopeService().prepare(
system_prompt=system_prompt,
messages=candidate_messages,
structured_memory=session.structured_memory,
tool_schemas=allowed_tools,
window_tokens=context_window_tokens,
)
ContextEnvelopeService 会保留最近 6 个完整对话轮,以及尚未形成完整轮次的尾部消息。它不会简单截取最后 N 条消息,因为最后 N 条可能从一个 assistant 回答中间开始,使问题和回答失去对应关系。
完成装配后,Envelope 把安全处理后的记忆追加到 System Prompt,把裁剪后的消息交给 LangChainChatAgentRunner。Runner 最终只接收已经准备好的 request.system_prompt 和 request.messages,不再自行读取整段数据库历史。
3. 60%、85% 和 95% 分别代表什么
当前自适应记忆采用三档处理,但三者发生在不同阶段:
| 阈值 | 行为 | 目的 |
|---|---|---|
| 60% | 一轮对话完成并刷新用量后,调度后台结构化压缩 | 提前整理旧历史,不阻塞当前回答 |
| 85% | 下一轮请求装配前,同步压缩六个最近轮次之前的旧消息 | 避免高占用请求直接撞上硬上限 |
| 95% | Context Envelope 拒绝继续执行并返回 CHAT_CONTEXT_LIMIT_REACHED | 为输出和运行波动保留最后安全空间 |
后台压缩任务使用稳定 Job ID,最多重试三次,并通过 PostgreSQL Compare-And-Set 更新 memory_summary_version。如果两个 Worker 同时基于旧版本生成记忆,只有版本仍匹配的结果能够写入;较晚到达的旧结果不会覆盖新记忆。
需要注意,压缩并不删除原始聊天记录。memory_through_message_id 只表示“哪些消息已经被记忆覆盖”,完整消息仍然保存在数据库中,用于会话展示和审计。
4. 结构化记忆保存什么,又拒绝保存什么
早期自由文本摘要有一个明显问题:模型可能把摘要中的命令式文本当成系统指令,也无法区分“用户确认的事实”和“助手提出的猜测”。当前结构化记忆只允许六类数据:
- 用户目标
user_goals; - 已确认事实
confirmed_facts; - 用户偏好
preferences; - 已形成的决策
decisions; - 未完成事项
open_tasks; - 资源引用
resource_refs。
每一项都必须携带来源消息 ID、可选 Citation ID 和信任级别。信任级别区分用户陈述、用户确认、工具证实和助手提议;其中 confirmed_facts 只接受用户确认或工具证实的内容,助手自己提出的判断不能直接升级成事实。
压缩 Prompt 也明确禁止保存隐藏推理、Prompt、凭据、原始工具输出和恢复安全状态。Pydantic 模型会再次检查字段白名单、单项长度、总大小和疑似指令文本。进入 Context Envelope 后,系统还会过滤 executionPermitted、recoveryMode、validatorStatus 等安全状态标记。
这意味着记忆可以提醒模型“用户正在排查哪个任务”,但不能告诉系统“之前已经允许自动恢复”。真正的恢复权限必须重新从当前结构化报告和 Policy Gate 获取。
旧版自由文本摘要仍可在迁移期被读取,但会被包在明确的不可信数据说明中:它只能作为引用,不能作为系统指令或已确认事实。结构化记忆生成失败时,压缩状态会变成 degraded,而不是把无法校验的模型输出写入数据库。
5. Skill 和 RAG 怎样进入上下文
Skill 使用渐进式披露。System Prompt 初始只包含当前已选择 Skill 的名称和描述;模型判断某个 Skill 与任务匹配后,必须调用 load_skill 获取完整正文。这样避免每轮对话重复装入全部 Skill 指令。
RAG 则作为 knowledge_retrieval 工具进入 ReAct。Query Rewrite、Milvus 向量召回、BM25L、RRF、Rerank 和 Citation 都发生在工具内部,返回的是本轮 Observation,而不是永久写入系统 Prompt。只有经过后续记忆压缩且满足来源与信任规则的信息,才有机会成为跨轮记忆。
因此,三者的生命周期不同:
Skill 描述:启动时进入能力目录
Skill 正文:按需加载到当前轮
RAG 结果:作为当前轮工具 Observation
Structured Memory:经过验证后跨轮复用
九、一次请求在系统里究竟怎样流动
以“查询某个知识库中的数据库死锁处理方式”为例,完整时序如下:
Client API / Run Router & Policy LangChain Agent RAG Tool
| | | | |
| POST message | | | |
|---------------->| persist user/run | | |
| |-------------------->| safety + intent | |
| |<--------------------| bounded_react | |
| | compile context/tool allowlist | |
| |------------------------------------------>| model decides |
| | | |----------------->|
| | | | citation results |
| | | |<-----------------|
|< - - - - - - - | SSE tool/citation/content events | |
| | persist public events + audit | |
|<----------------| complete | |
如果请求换成“查看 diagnostic_xxx”,Policy 会选择 direct_read,中间的 LangChain Agent 和 RAG Tool 都不会出现。如果换成恢复请求,则在生成 Pending Action 后停止,等待用户确认。
这个差异正是混合架构的价值:同一个聊天入口可以提供统一体验,但内部执行链不必为了“看起来像 Agent”而强行一致。
十、为什么 SSE 不是持久化方案
SSE 适合把内容增量、工具状态、引用、确认请求和完成事件实时展示给前端,但连接可能中断。若只依赖当前 HTTP 进程,一次较长的模型调用或工具调用就可能随着断线丢失。
因此,当前 Chat Run 运行在已有的持久后台任务框架上:
- PostgreSQL 中先保存 Run 和用户消息;
- Worker claim 当前 attempt,并写入
run.status或run.restarted; - 执行 Chat Streaming Service,把公共事件追加到 Run Event;
- 使用稳定的 assistant message ID 防止重试时重复写回答;
- 对错误分类,区分可重试失败与终止失败;
- 完成时原子保存结果与 complete event。
因此,两者承担不同职责:
SSE:当前连接中的实时投影
PostgreSQL Run/Event:可恢复、可重放的事实来源
Tool Audit:工具实际执行过程的审计记录
用户断线后重新连接,应该从持久事件继续读取,而不是要求模型重新思考一遍。Worker 重启后,也应从 Run 状态和幂等记录恢复,而不是重复执行有副作用的工具。
十一、Chat ReAct Agent 与 LangGraph AIOps Agent 有什么不同
项目中同时使用 LangChain Chat Agent 与 LangGraph AIOps Agent,并不是重复建设。它们解决的是两类不同的不确定性。
| 对比项 | Chat ReAct Agent | LangGraph AIOps Agent |
|---|---|---|
| 主要输入 | 开放式自然语言请求 | 已进入诊断域的事故与证据范围 |
| 不确定性 | 下一步是否需要工具、选哪个工具 | 证据是否充分、假设是否成立、是否需要重规划 |
| 主控制方式 | Router 后的短 ReAct 循环 | 显式状态图和条件边 |
| 步骤顺序 | 允许模型在预算内动态决定 | Planner、Executor、Evidence Gate、Decision 等强依赖 |
| 状态 | 对话消息与短期 Observation | 诊断状态、证据、假设、Validator、Checkpoint |
| 高风险动作 | 创建待确认操作 | Recovery Planner + Validator Router + Policy Gate |
| 适合场景 | 问答、知识检索、统一入口 | 故障调查、差分假设、恢复决策与报告 |
当前 AIOps 图包含 Planner、Executor、Fact Adapter、Sufficiency Gate、Hypothesis Adjudicator、Replanner、Decision、Deterministic Validator、可选 LLM Validator、Recovery Planner、Policy Gate 和 Report。复杂场景还可以通过 Strategy Router 分发 Specialist,再聚合证据。
这条链路的关键是节点之间存在强顺序和可验证状态。例如,证据不足时必须回到 Executor 或 Replanner;确定性验证失败时不能直接进入恢复;语义风险较高时才路由到 LLM Validator;最后仍由 Policy Gate 决定是否允许执行。
因此,选择 ReAct 还是 LangGraph,不应该变成“哪个框架更强”的争论。即便 LangChain 的高层 Agent 在内部也可能使用图运行时,项目层面的选择仍然是:
- 路径主要由模型动态决定,使用受控 ReAct;
- 路径必须被业务状态和安全门严格约束,显式建模为 LangGraph。
Chat Agent 负责理解入口请求并把用户带到正确能力,AIOps Agent 负责完成证据驱动的长流程诊断。
十二、怎样验证这套架构不是“只看起来合理”
只测试最终回复文本,很难发现权限和编排错误。一个回答可能语义正确,却调用了禁止工具;也可能没有完成必需工具调用,只靠模型猜出了答案。
当前 Conversation Eval 因此分别检查:
- Intent 与执行模式是否正确;
- 目标资源 ID 是否正确提取;
- 允许工具精度与必需工具召回;
- Direct Read 是否真正绕过 ReAct;
- 高风险操作是否进入确认;
- 调用预算是否被遵守;
- 结果是否包含必要 grounding;
- 幂等、跨租户隔离与 SSE 重放是否正确。
同时设置硬门禁:禁止工具调用、跨租户访问、未确认写操作、预算后调用、安全字段不一致、自动执行恢复,以及向公共事件泄露隐藏推理,只要出现一次就不能判定通过。
这类 Eval 的目标不是证明模型“聪明”,而是验证系统控制面在模型输出变化时仍然成立。
总结:生产化的重点不是让 ReAct 做得更多
回到开头的三个请求:知识问题可以进入有预算的 ReAct,状态查询直接读取结构化事实,恢复请求停在人工确认边界。它们共享一个聊天入口,却不共享同一种执行方式。
这套架构最重要的不是使用了多少 Agent 名词,而是完成了职责分离:
- Router 判断请求属于哪个能力域;
- Execution Policy 决定是否允许 ReAct、允许哪些工具以及预算多少;
- LangChain 承担模型—工具—Observation 的通用循环;
- Tool Bridge 注入身份、处理幂等并隔离副作用;
- Context Envelope 控制 Prompt、Skill、Memory 和 Observation;
- PostgreSQL Run/Event 与 Audit 提供恢复和审计;
- LangGraph 承担强顺序、证据驱动的 AIOps 诊断链路。
如果要继续深入这套系统,接下来值得追问的不是“要不要再加一个 Agent”,而是三个更具体的问题:工具目录是否与实际权限完全一致?失败重试能否证明不会重复产生副作用?一次回答是否能还原为公开证据、执行事件和明确的安全决策?
这些问题,才是 Chat Agent 从可演示走向可运行的分界线。