
1. 这不是概念炒作是工程现场的七把手术刀“AI Agent”这个词最近被讲得太多太多人把它当成一个玄乎的黑箱——要么说它“像人一样思考”要么说它“能自主完成任务”结果一动手就卡在第一步连个能跑起来的最小闭环都搭不稳。我带过六支不同行业的Agent落地团队从金融风控到工业质检从电商客服到科研辅助踩过的坑比写过的代码还多。今天这篇不谈哲学、不画架构图、不列论文引用只拆解你在真实工程中每天要面对的七个硬核决策点——它们不是理论模型里的“要素”而是你敲下第一行代码前必须拍板的七道闸门。核心关键词AI Agent、Agent、LLM、工具、循环机制这五个词不是并列关系而是层层咬合的齿轮LLM 是推理引擎工具是执行肢体循环机制是呼吸节律而 Agent 本身就是这套系统在具体业务场景里跑起来时的完整工程实体。你不需要先搞懂“什么是智能”你需要知道当用户发来一句“查一下上季度华东区销售额并对比竞品A的官网披露数据”你的系统到底该让哪段代码先动、调哪个API、怎么判断结果可信、失败了往哪回滚、要不要记日志、下次再遇到类似请求能不能快0.3秒——这些全落在那七个决策点上。适合谁读如果你正在用 LangChain 写 chain 却总在.invoke()里报错如果你用 LlamaIndex 做 RAG但用户一问“为什么上次说的数据和这次不一样”你答不上来如果你刚部署完一个基于 Ollama 的本地 Agent结果它对着 Excel 文件反复说“我需要更多上下文”却死活不调 pandas或者你正纠结该选 Rust 还是 Python 做底层调度——那你不是缺理论是缺一张标着红叉与箭头的工程地图。这张地图不教你“如何成为AI科学家”只告诉你“这里要加熔断这里必须做 schema 校验这里不加重试会丢数据这里漏了 trace ID 就没法追问题。” 它来自产线不是实验室。2. 七要素 ≠ 七模块要素是决策锚点不是代码目录很多资料把 AI Agent 拆成“记忆、规划、工具调用、执行、反思、学习、交互”七要素听起来很完整但落到工程里全是陷阱。比如“记忆”——你是用 Redis 缓存对话历史还是用 Chroma 存向量或是直接把整个 session 放进 LLM 的 prompt这根本不是选“记忆模块”而是在决定系统对状态一致性的容忍度、冷启动延迟的上限、以及运维复杂度的天花板。同样“工具调用”不是挑个Tool类继承一下就完事而是要回答工具返回结构化数据还是非结构化文本失败时是抛异常还是返回 error 字段工具链是否支持异步并发有没有超时熔断参数校验放在 LLM 输出解析层还是工具封装层——每一个选择都在给后续的调试成本、扩展难度、线上稳定性埋雷。所以我把这七个点重新定义为工程决策锚点Engineering Decision Anchors每个锚点背后都对应一组不可回避的技术权衡决策点1目标锚定Goal Binding不是“设定目标”而是决定目标如何被表达、验证、分解。是用自然语言描述易写难验还是用 JSON Schema 约束难写易验目标是否允许动态嵌套如“先查销量再根据结果决定是否查库存”目标变更时是中断当前流程重来还是注入新指令继续我在某银行反洗钱项目里吃过亏最初用纯文本目标结果 LLM 把“识别高风险交易”理解成“列出所有交易”导致下游规则引擎直接过载。后来强制目标必须含action,entity,condition三字段用 Pydantic 做 runtime 校验错误率下降 72%。决策点2LLM 接口契约LLM Interface Contract这是所有混乱的源头。你调的不是“一个大模型”而是“一个有明确输入输出协议的服务”。协议必须定义输入 prompt 的模板结构system/user/assistant 分段是否固定是否允许插入 tool description、输出格式约束必须 JSON是否允许 markdownerror 字段命名规范、token 预留策略预留多少 token 给 tool call response、流式响应处理逻辑chunk 边界如何识别中间 abort 如何处理。我们曾用同一个 Llama3-70B 模型在两个项目里表现天壤之别一个项目用 OpenRouter 的标准 API另一个自建 vLLM 服务但没统一 prompt template结果后者在工具调用时频繁出现 JSON 解析失败——不是模型不行是契约没签清楚。决策点3工具注册与发现Tool Registration Discovery“引入工具类”不是加个tool装饰器就完事。真正的难点在于工具元信息如何描述名称、描述、参数 schema、副作用标记、权限等级工具列表是静态配置还是动态加载如插件目录扫描LLM 生成的 tool name 是精确匹配还是支持 fuzzy search工具调用失败时是否允许 LLM 自行修正参数重试我们在做政务知识库 Agent 时要求工具必须标注is_sensitive: true触发时需二次确认而天气查询工具则标记is_cacheable: true自动走 Redis 缓存。这些元信息必须在注册时就固化不能靠 LLM 临场判断。决策点4循环控制粒度Loop Control Granularity“循环机制”不是 while True而是决定单次循环的原子性边界。一次 loop 是“LLM 输出 → 工具调用 → LLM 再输出”还是“LLM 输出 → 多工具并发 → 汇总 → LLM 再输出”loop 的退出条件是什么成功标志最大步数时间阈值人工干预信号loop 中间状态是否可中断、可回溯、可审计我们做过实验将 loop 粒度从“单工具调用”改为“多工具批处理”在电商比价场景下平均响应时间从 8.2s 降到 3.7s但错误定位难度翻倍——因为失败日志里不再显示“第3步调用支付接口失败”而是“批处理组#5 执行异常”。最终方案是默认细粒度 loop仅对已验证稳定的工具组合开放批处理开关并强制记录每一步的输入输出哈希。决策点5状态持久化策略State Persistence StrategyAgent 的“记忆”本质是状态管理。你要选全量 session 存 DB强一致性高延迟还是关键 state 存 Redis快但可能丢失或是只存 trace ID 最终结果无状态依赖外部系统补全状态序列化用 JSON 还是 Protocol Buffers是否压缩过期策略是 TTL 还是 LRU某物流调度 Agent 曾因状态存 Redis 且未设 TTL导致缓存雪崩——凌晨批量下单时旧 session 占满内存新请求全部 fallback 到慢速 DB 查询。解决方案是状态分两级短期上下文5min存 Redis 并设 TTL长期业务状态如订单ID、用户偏好存 PostgreSQL用 CDC 同步到搜索库。决策点6容错与降级路径Fault Tolerance Fallback Paths“自主容错控制”不是让 LLM 自己想辙而是预埋确定性逃生通道。降级路径必须明确LLM 失败时是否启用规则引擎兜底工具超时时是重试、跳过、还是返回预设提示当多个工具连续失败是否触发人工审核队列我们在医疗问答 Agent 中对“药品禁忌症查询”工具设置了三级降级1主工具失败 → 2调用药品说明书 PDF 解析服务 → 3返回结构化 FAQ 列表 “建议咨询药师”提示。关键点在于所有降级路径的输入输出格式必须与主路径对齐否则 LLM 无法无缝衔接。决策点7可观测性注入点Observability Injection Points没有 trace、log、metric 的 Agent 就是黑盒。你必须在七个位置埋点1目标解析开始/结束2LLM 请求发出/响应接收3工具调用发起/返回4循环迭代计数5状态读写时刻6降级路径触发7最终响应组装。每个埋点至少含trace_id、span_id、component、status、duration_ms、input_hash、output_hash。我们曾用 Jaeger 追踪一个客服 Agent发现 83% 的延迟来自 LLM 的 prompt 渲染阶段而非 inference原因是每次都将整个知识库 chunk 拼进 prompt——优化后改为按需检索动态注入P95 延迟从 12s 降至 2.4s。这七个决策点没有标准答案。选 Rust 还是 PythonRust 在决策点4循环控制和决策点6容错上天然优势——所有权模型杜绝空指针async/await 对并发控制更精细但 Python 在决策点3工具注册和决策点7可观测性生态更成熟LangChain OpenTelemetry 组合开箱即用。关键不是语言之争而是看清每个决策点背后的真实约束你的团队熟悉度、现有基础设施、SLA 要求、合规审计需求。3. 实操拆解从零搭建一个可审计的销售分析 Agent现在我们用一个真实场景——“销售分析 Agent”——把七个决策点串成一条可运行的流水线。这个 Agent 的需求很朴素用户输入“查华东区Q2销售额”它要能1识别区域、时间、指标2调用 BI 系统 API 获取数据3用 pandas 做简单同比计算4生成带图表的 Markdown 报告5记录全过程供审计。不追求炫技只确保每一步都可控、可查、可修复。3.1 决策点1目标锚定——用 Pydantic 强约束代替自由发挥我们放弃让 LLM 自由输出目标描述而是定义一个SalesQuerySchemafrom pydantic import BaseModel, Field from typing import Optional, List class SalesQuery(BaseModel): region: str Field(..., description销售区域如华东区、华北区) quarter: str Field(..., description季度格式为Q1、Q2等) metric: str Field(defaultsales, description指标如sales、orders、avg_order_value) compare_with: Optional[str] Field( None, description对比对象如Q1、去年同期、竞品A ) output_format: str Field(defaultmarkdown, description输出格式markdown或json)关键设计region和quarter是必填避免 LLM 胡猜compare_with用 Optional允许无对比场景output_format显式声明后续渲染逻辑直接受控。LLM 的 system prompt 只有一句“你是一个严格的参数提取器。请严格按 SalesQuery JSON Schema 输出不要任何额外字符。”实测下来JSON 解析失败率从 31% 降到 0.7%。这不是 LLM 变强了是你把它的发挥空间锁死了——工程上可控性永远优于灵活性。3.2 决策点2LLM 接口契约——vLLM 自定义 tokenizer我们用 vLLM 部署 Qwen2-7B但关键在 prompt engineeringSYSTEM_PROMPT 你是一个销售数据分析助手。请严格按以下 JSON Schema 输出 {schema} 不要任何解释、不要markdown、不要额外空格。 USER_PROMPT 请解析以下用户请求 {user_input} 输出 JSON重点在tokenizer层面我们修改了 Qwen 的 tokenizer为SalesQuery的每个字段添加特殊 token如region、/region并在 prompt 中强制包裹。这样 LLM 学习到region华东区/region是一个原子单元不会被切开。同时vLLM 的max_model_len设为 8192但预留 2048 token 给 tool response确保即使返回大表格也不会 truncation。提示不要相信 LLM 的“自我宣称”。我们曾发现某模型在 prompt 里写“我严格遵守 JSON 格式”实际输出却带注释。唯一可靠的是1prompt 结构化2output parser 做 schema 校验3失败时 fallback 到规则提取正则匹配“华东区”、“Q2”等关键词。3.3 决策点3工具注册与发现——YAML 描述 动态加载工具不写死在代码里而是用 YAML 描述# tools/sales_api.yaml name: sales_api description: 查询BI系统销售数据 parameters: region: {type: string, required: true} quarter: {type: string, required: true} metric: {type: string, default: sales} side_effects: [network_call] permissions: [read_sales_data] timeout: 15加载逻辑def load_tools_from_dir(tool_dir: str) - Dict[str, Tool]: tools {} for yaml_file in Path(tool_dir).glob(*.yaml): with open(yaml_file) as f: spec yaml.safe_load(f) # 动态绑定函数 func getattr(sales_module, spec[name]) tools[spec[name]] Tool( namespec[name], funcfunc, descriptionspec[description], parametersspec[parameters], timeoutspec[timeout] ) return tools好处新增工具只需加 YAML 文件无需改 Python 代码权限、超时等元信息集中管理审计时直接导出 YAML 即可。3.4 决策点4循环控制粒度——单步原子 批处理开关我们的 loop 主体def run_agent(user_input: str) - str: state State() # 包含 query, history, tools, etc. for step in range(MAX_STEPS): # Step 1: 目标解析 query parse_query(user_input, state.llm) # Step 2: 工具选择单工具 tool_name, tool_args select_tool(query, state.tools) # Step 3: 工具执行带熔断 try: result execute_tool(tool_name, tool_args, timeoutstate.tools[tool_name].timeout) except ToolTimeoutError: result {error: timeout, tool: tool_name} # Step 4: LLM 汇总输入query result final_response summarize_result(query, result, state.llm) if is_final_answer(final_response): return final_response # 更新 state 供下次 loop 使用 state.update(query, result, final_response) return 超出最大步骤请简化问题关键点每次 loop 只调一个工具保证失败可定位execute_tool内部用concurrent.futures.TimeoutError做熔断不依赖 LLM 判断超时summarize_result的 prompt 明确要求“如果结果含 error 字段请直接返回‘工具调用失败’不要尝试解释”。3.5 决策点5状态持久化策略——Redis PostgreSQL 双写State 类设计class State: def __init__(self, session_id: str): self.session_id session_id self.redis_client redis.Redis(...) self.pg_conn psycopg2.connect(...) def save(self, key: str, value: Any): # 双写Redis 存短期TTL30minPG 存长期 self.redis_client.setex(fstate:{self.session_id}:{key}, 1800, json.dumps(value)) self.pg_conn.execute( INSERT INTO agent_state (session_id, key, value, created_at) VALUES (%s, %s, %s, NOW()), (self.session_id, key, json.dumps(value)) ) def load(self, key: str) - Any: # 优先读 Redis失败读 PG val self.redis_client.get(fstate:{self.session_id}:{key}) if val: return json.loads(val) # fallback to PG...审计时DB 表agent_state记录所有关键状态变更配合 trace_id 关联日志可还原任意一次会话的完整决策链。3.6 决策点6容错与降级路径——三层 fallback工具执行链def execute_tool(name: str, args: dict) - dict: try: # 主路径调用 BI API return bi_api.query(**args) except TimeoutError: # 降级1查缓存 cache_key fsales:{args[region]}:{args[quarter]} cached redis.get(cache_key) if cached: return json.loads(cached) else: # 降级2返回预设模板 return { error: data_unavailable, suggestion: BI系统暂不可用可查看历史报告或稍后重试 } except Exception as e: # 降级3记录错误触发告警 logger.error(fTool {name} failed: {e}) alert_slack(fAgent tool failure: {name} | {args}) return {error: system_error, contact: opscompany.com}注意所有降级返回的dict结构一致summarize_result函数无需修改即可处理。3.7 决策点7可观测性注入点——OpenTelemetry 全埋点我们在每个关键函数开头加 decoratorfrom opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider tracer trace.get_tracer(__name__) def trace_step(step_name: str): def decorator(func): def wrapper(*args, **kwargs): with tracer.start_as_current_span(step_name) as span: span.set_attribute(session_id, kwargs.get(session_id, unknown)) span.set_attribute(step_input, str(args[:2])) # 避免敏感数据 result func(*args, **kwargs) span.set_attribute(step_output_hash, hashlib.md5(str(result).encode()).hexdigest()) return result return wrapper return decorator trace_step(parse_query) def parse_query(user_input: str, llm: LLM) - SalesQuery: ...Jaeger UI 中一个会话的 trace 长这样├─ parse_query (210ms) │ ├─ llm_invoke (180ms) │ └─ schema_validate (30ms) ├─ select_tool (12ms) ├─ execute_tool:sales_api (420ms) │ ├─ api_call (390ms) │ └─ cache_check (30ms) └─ summarize_result (150ms)点击任意 span能看到输入哈希、输出哈希、错误堆栈——这才是真正的“可审计”。4. 常见问题与排查技巧实录那些文档里不会写的坑做 Agent 工程80% 的时间花在解决“看似不该出问题”的地方。以下是我在六个项目里高频遇到的问题附真实排查路径和绕过方案。4.1 问题1LLM 总在工具调用后“忘记”自己刚干了什么现象用户问“华东区Q2销售额是多少”Agent 调用 API 返回{sales: 1250000}但下一步 LLM 却说“我需要更多信息才能回答”仿佛没看到返回值。排查路径第一步检查 prompt 中 tool response 的插入位置。常见错误是把 response 插在 user message 末尾但 LLM 的 context window 有限response 被 truncation。第二步验证 response 是否被 tokenizer 正确编码。用tokenizer.encode(response)看长度若 2048必然丢失。第三步确认 LLM 的 chat template 是否支持toolrole。Qwen2 支持但 Llama3 默认不支持需手动 patch。实操心得我们最终方案是——永远把 tool response 放在 assistant message 里并用特殊分隔符包裹|assistant|我已调用 sales_api 工具获取数据 tool_response {sales: 1250000} /tool_response 请基于此回答用户问题。同时在 vLLM 的--max-model-len里预留足够空间并在代码中做 response 长度截断保留最后 1024 tokens。这比指望 LLM 记住长文本靠谱得多。4.2 问题2工具返回 JSON但 LLM 解析出错死循环现象工具返回{error: invalid_region}LLM 却忽略 error 字段继续调用下一个工具形成无限 loop。根因LLM 的“工具调用”能力本质是文本生成它不理解 JSON 结构。当 response 含 error它可能生成{name: next_tool, ...}而非停止。解决方案在execute_tool后强制做 schema 校验而非信任 LLMdef safe_execute_tool(name: str, args: dict) - dict: result execute_tool(name, args) # 强制校验必须含 data 或 error 字段 if error in result: return {error: result[error], tool: name} elif data not in result: return {error: invalid_response_format, tool: name, raw: str(result)} else: return {data: result[data]}然后在 loop 中只要result[error]非空立即 break 并返回。这是最笨但最稳的方法——把 LLM 当作不可信的协作者而非决策者。4.3 问题3本地部署的 AgentCPU 占用 100%但 QPS 低得可怜现象用 Ollama 运行 Llama3-8Btop 显示 CPU 100%但并发请求只有 2 QPS远低于预期。排查发现Ollama 默认用 llama.cpp 的--numa模式但在非 NUMA 架构服务器上反而降低性能且--num_ctx设为 4096导致每次推理都分配大内存块。绕过方案改用ollama run --gpu all即使无 GPUllama.cpp 的 CUDA backend 在 CPU 上也更快--num_ctx降为 2048用--batch-size 512提升吞吐关键加--keep-alive 5m避免每次请求重建 context。实测QPS 从 2.1 提升到 18.7CPU 占用降至 65%。这不是模型问题是 runtime 配置问题。4.4 问题4Agent 在生产环境突然变“傻”相同输入返回不同结果现象测试时一切正常上线后同一用户问“Q1销售额”有时返回数字有时返回“请提供更具体的时间范围”。根因LLM 的 temperature 参数在生产环境被意外设为 0.8开发环境是 0.0。temperature0.8 会让输出随机而销售数据必须确定。排查技巧在所有 LLM 调用处强制打印 temperature 和 seed到 debug log用curl -X POST http://localhost:8000/v1/chat/completions手动测试对比 prod/dev 的 raw response检查环境变量OLLAMA_NUM_GPU在 prod 为 0导致 fallback 到 CPU 模式而 CPU 模式下某些量化版本的 deterministic behavior 不同。经验生产环境 LLM 必须设temperature0并固定seed。所谓“智能”在工程里首先是“确定性”。4.5 问题5工具调用成功但返回数据格式不一致LLM 解析失败现象BI 系统 API 有时返回{data: [{region: 华东, sales: 1250000}]}有时返回{data: {region: 华东, sales: 1250000}}LLM 的 JSON parser 频繁崩溃。根本解法在工具封装层做标准化适配器而非让 LLM 适应def sales_api_adapter(raw_response: dict) - dict: if isinstance(raw_response.get(data), list): # 转为单对象假设只查一个区域 if raw_response[data]: return {data: raw_response[data][0]} else: return {error: no_data_found} elif isinstance(raw_response.get(data), dict): return raw_response else: return {error: unexpected_response_format, raw: str(raw_response)}所有工具返回必须符合{data: {...}}或{error: ...}LLM 只需处理这两种 case。适配器代码比写十个 prompt 更可靠。4.6 问题6Agent 日志里全是 trace_id但找不到对应用户请求现象Jaeger 里 trace 很多但运营同学说“用户反馈昨天下午3点报告错误”你却无法关联。破局点在用户入口处强制注入 business_idapp.post(/ask) def ask_endpoint(request: AskRequest): # 从 request header 或 body 提取业务标识 business_id request.headers.get(X-Business-ID) or request.user_id # 创建 trace 并注入 tracer trace.get_tracer(__name__) with tracer.start_as_current_span(user_request, attributes{business_id: business_id}): # ... agent logic然后在 Jaeger 搜索框输入business_id U123456立刻定位。没有 business_id 的 trace一律视为无效日志自动丢弃。注意business_id 必须是业务侧可控的标识如订单号、用户UID不能是随机 UUID。否则运营无法使用。5. 工具链选型实战指南不是越新越好是越稳越香选工具不是赶时髦而是算三笔账学习成本账、维护成本账、故障成本账。下面是我用过的主流组合按场景推荐。5.1 LLM 运行时vLLM vs Ollama vs Text Generation InferenceTGI维度vLLMOllamaTGI吞吐量QPS★★★★★PagedAttention★★☆llama.cpp 优化有限★★★★FlashAttention启动速度★★☆需编译★★★★★docker run 一行★★☆需 k8s 部署GPU 利用率★★★★★显存复用★★☆常驻进程吃显存★★★★动态 batch调试友好度★★☆日志较晦涩★★★★★ollama logs直观★★★Prometheus metrics 全适用场景高并发生产环境100 QPS快速原型、本地开发云原生平台K8s Istio我的选择内部 PoC用 Ollamaollama run qwen2:7b5分钟起手客户交付用 vLLMDocker Compose Nginx 负载均衡QPS 稳定在 85拒绝 TGI除非客户已有全套 K8s 生态否则运维成本太高。5.2 Agent 框架LangChain vs LlamaIndex vs 自研调度器维度LangChainLlamaIndex自研Rust工具链丰富度★★★★★200 integrations★★★☆专注 RAG★★☆需自己造轮子循环控制灵活性★★☆LCEL 复杂逻辑难写★★☆非设计目标★★★★★match 表达式精准控制错误追踪深度★★★callback 机制★★☆日志较浅★★★★★panic backtrace 直达源码内存安全★★☆Python GC 不可控★★☆同上★★★★★Rust 编译期杜绝 use-after-free团队技能匹配★★★★★Python 工程师全覆盖★★★★需熟悉 vector store★★☆Rust 学习曲线陡真实案例某车企项目要求“绝对不能因 Agent 崩溃导致产线停机”我们选 Rust 自研调度器。虽然开发多花 3 周但上线后 6 个月零 crash而隔壁用 LangChain 的项目每月平均 2.3 次 OOM。故障成本账算下来自研更便宜。5.3 观测工具OpenTelemetry Jaeger vs Datadog APM维度OpenTelemetry JaegerDatadog APM部署成本★★★★★开源可私有部署★★☆SaaS 月费 $15/主机定制埋点自由度★★★★★完全可控★★★☆受限于 agent SDKTrace 分析深度★★★★可查 span 内存占用★★★★UI 更友好告警集成★★☆需接 Prometheus Alertmanager★★★★★内置告警策略合规性★★★★★数据不出内网★★☆需签 DPA部分行业禁用我的实践金融客户必须私有化用 OTel Jaeger VictoriaMetrics创业公司快速验证用 Datadog省下的运维时间用来多跑 3 个 AB 测试。5.4 工具开发FastAPI vs Actix-webRust vs GinGo维度FastAPIPythonActix-webRustGinGo开发速度★★★★★Pydantic async★★☆生命周期管理复杂★★★★路由简洁并发性能★★★☆asyncio 优秀★★★★★零拷贝 actor★★★★goroutine 轻量错误处理★★★try/except★★★★★Result/Option 强制★★★★error return 显式生态兼容★★★★★NumPy/Pandas 无缝★★☆科学计算库少★★★☆需 cgo 调 C 库选型逻辑数据处理密集型如 pandas 计算→ FastAPI高频 IO 密集型如万级 webhook 接收→ Actix-web混合场景既要调 Python 工具又要高并发→ Gin subprocess 调 Python。6. 安全与合规Agent 不是玩具是生产系统“Agent 安全”不是加个防火墙而是把安全当作七个决策点的默认约束。6.1 决策点1目标锚定的安全加固输入净化在parse_query前用bleach.clean()过滤 HTML/JS防止 XSS 注入 prompt敏感词拦截构建行业敏感词库如“银行卡号”、“身份证”匹配即拒绝不进 LLM意图白名单只允许SalesQuery中定义的region值华东、华北...其他一律报错。6.2 决策点3工具注册的权限隔离每个工具 YAML 必须含permissions字段在execute_tool前校验当前 session 的 RBAC 权限if not has_permission(session_user, tool_spec[permissions]): raise PermissionDenied(fUser {session_user} lacks permission {tool_spec[permissions]})权限变更实时生效无需重启服务。6.3 决策点4循环控制的资源熔断为每个 session 设置max_steps5max_total_time30s用threading.Timer做硬超时一旦触发强制 kill 当前 loop 线程记录熔断事件到审计表供安全团队分析。6.4 决策点5状态持久化的加密存储Redis 中的 state 值用 AES-256 加密密钥由 KMS 托管PostgreSQL 的 agent_state