AI Agent工程化落地:ReAct协议守卫与MCP执行层实战 1. 这不是又一个“调 API”的玩具——它是一套能真正跑起来的 AI Agent 工程骨架“别再只会调 API 了”——这句话我去年在三个不同技术分享会上都听人说过每次说完台下都笑。笑完之后呢大家还是回去写requests.post(url, jsonpayload)把大模型当高级文本补全器用。直到上个月我给一家做工业设备远程诊断的客户搭一套现场知识助手才真正被逼到墙角用户要的不是“请用中文总结这段日志”而是“查一下2024年7月12号下午3点那台编号PLC-8827的变频器报错代码F305对照《西门子S120故障手册V3.2》第47页判断是否需要停机并生成维修建议发给张工”。这已经不是 prompt 工程能解决的问题了这是典型的多步骤、带工具调用、需状态追踪、要结果验证的智能体行为。我翻遍了 LangChain 官方文档、LangGraph 的示例库、甚至把 ReAct 论文原文逐段重读发现一个问题所有“入门教程”都在教你如何让 LLM “思考”却没人告诉你当它真开始思考时系统会卡在哪、日志里会冒出什么诡异字符串、为什么同一个 function calling 配置在本地 Ollama 上跑得飞起在云端部署后却总超时300ms——而这300ms刚好是下游设备API网关的硬性熔断阈值。这篇东西就是我把过去四个月从零手搓一个可交付、可监控、可回滚的 AI Agent 框架过程中所有掉进过的坑、贴着边擦过去的雷、以及最终焊死在生产环境里的解决方案原样端出来。它不叫“LangChain 最佳实践”它叫“我在产线旁蹲了两周后写下的生存指南”。适合正在评估是否该上 Agent 架构的架构师、被产品追着问“能不能让AI自己查数据库”的后端同学以及刚学完 LangChain 基础、打开文档却不知道下一步该敲哪行代码的开发者。核心就一条让 Agent 不是 demo而是能嵌进你现有 CI/CD 流水线、能被 Prometheus 监控、能在 Grafana 里看到每一步耗时的工程组件。2. 为什么必须放弃“LangChain Prompt”老路——从 ReAct 到 MCP 的底层逻辑跃迁2.1 ReAct 不是思维链而是一套可执行的状态机协议很多人把 ReActReasoning Acting当成一种 prompt 写法比如加一句“请先思考再行动”。这是最大的误解。ReAct 的本质是定义了一套LLM 与执行环境之间的通信契约。它规定了当 LLM 需要外部信息或操作时必须输出严格格式化的 Action 字符串执行层收到后必须调用对应工具并返回结构化结果LLM 再基于结果继续推理。这个过程不是“让它想”而是“强制它按协议说话”。我最初用 LangChain 的AgentExecutor跑 ReAct结果在真实业务中崩了三次。第一次是 LLM 输出了Action: search_db{query: ...}但实际工具名是db_search大小写下划线全错第二次是它在 Action Input 里塞了未转义的 JSON 双引号导致 Pythonjson.loads()直接抛异常第三次最致命——它连续输出了两个Action:中间没夹Observation:整个状态机彻底失步。后来我才明白LangChain 的ReActSingleActionAgent默认只校验 Action 名称是否存在对输入格式、输出结构、状态流转完全不设防。这不是 bug是设计哲学差异LangChain 是“胶水框架”它假设你已有一套成熟的工具注册和错误处理体系而 ReAct 协议本身要求执行层必须承担协议守卫者的角色。提示真正的 ReAct 实现必须在 Action 解析层插入三道硬闸——语法校验正则匹配 Action 格式、语义校验检查工具名是否注册、参数是否符合 Pydantic Model、状态校验确认当前 step 允许触发 Action。少一道线上就可能出不可逆的流程断裂。2.2 Function Calling 是接口MCP 才是协议——为什么我们绕不开 MCPFunction Calling函数调用这个词最近被讲烂了但绝大多数教程只停留在“怎么让 LLM 生成 JSON 调用你的函数”。这就像教人开车只讲油门位置不讲交通规则。真正的瓶颈不在 LLM 会不会调而在调用请求发出后执行环境如何安全、可靠、可观测地完成它并把结果以 LLM 能理解的方式送回去。MCPModel Context Protocol正是为解决这个问题诞生的。它不是某个公司推出的私有标准而是由多家 AI 基础设施厂商包括 Ollama、Fireworks、Together 等共同推动的开放协议。它的核心价值在于把“调用函数”这件事从应用层逻辑下沉为基础设施层能力。举个具体例子当你用 LangChain 调用一个数据库查询工具时传统做法是写一个 Python 函数里面包含连接池管理、SQL 注入防护、超时控制、重试策略。而 MCP 的思路是——你只需声明这个工具的 OpenAPI SpecYAML 格式然后由 MCP Server如mcp-server-ollama负责加载、验证、执行、记录。你的 Agent 代码里连import psycopg2都不需要出现。我踩的第一个深坑就是试图在 LangChain Agent 里硬塞 MCP。结果发现 LangChain 的Tool类型和 MCP 的ToolRequest结构根本对不上LangChain 要求工具返回字符串MCP 要求返回带status、content、error字段的 JSON 对象。最后我放弃了“兼容”直接用httpx.AsyncClient封装了一层 MCP Client所有工具调用统一走/tool/execute接口。这样做的好处是当客户突然要求把数据库查询换成调用他们自研的 OPC UA 设备服务时我只需要改 MCP Server 的配置文件Agent 代码一行不动。这才是工程可维护性的起点。2.3 LangChain 是乐高积木LangGraph 是施工图——但地基得你自己打LangChain 和 LangGraph 的关系常被类比为“积木”和“图纸”。这个比喻很准但漏掉了最关键的一点图纸上不会标出水泥标号、钢筋直径、地基深度。LangGraph 的StateGraph确实能帮你画出 Agent 的状态流转图但它默认的InMemoryStore在生产环境里就是个定时炸弹——没有持久化、没有并发锁、节点崩溃后状态全丢。我们上线第一版时用的就是 LangGraph 的内存状态。结果某天凌晨三点一个诊断任务执行到“生成维修报告”环节时服务器因内存溢出重启。等运维拉起服务那个任务的状态已经消失系统既不能重试因为不知道执行到哪一步也不能通知用户失败因为状态丢失。最后只能人工翻日志手动补发邮件。那次事故后我花了整整一周重写状态管理模块用 Redis Stream 存储每一步的state_id、node_name、input、output、timestamp用 Lua 脚本保证状态更新的原子性再加一层 Kafka Topic 做状态变更事件广播。这些事 LangGraph 不做也不该它做——它的职责是定义“怎么流转”而“流转过程怎么不死”是你的责任。注意LangGraph 的checkpointer是救命稻草但别迷信它的默认实现。BaseCheckpointSaver只存 Python 对象序列化后体积暴增RedisSaver虽好但 Redis 的HSET命令在高并发下有概率丢数据。我们最终采用PostgreSQLSaver用JSONB字段存状态配合ON CONFLICT DO UPDATE保证幂等这才是金融级可用的方案。3. 手搓框架的四大支柱从零构建可落地的 Agent 工程骨架3.1 支柱一协议守卫层——让 LLM 说人话更说“协议话”协议守卫层是整个框架的“海关”它不关心 LLM 多聪明只确保它输出的内容符合 ReAct/MCP 协议。这一层我拆成三个子模块1. Output Parser语法级守卫不用 LangChain 自带的ReActOutputParser而是手写正则解析器。关键点在于必须支持多轮 Action-Observation 交错。例如LLM 可能输出Thought: 需要查设备历史告警 Action: query_alarm_history Action Input: {device_id: PLC-8827, start_time: 2024-07-12T15:00:00Z} Observation: [{id: ALM-9921, code: F305, time: 2024-07-12T15:03:22Z}] Thought: F305 对应过压保护需查手册 Action: search_manual Action Input: {error_code: F305, manual_version: V3.2}LangChain 的 parser 会卡在第一个Action:后就返回后续内容全丢。我的解析器用re.findall(r(Thought|Action|Action Input|Observation): ([\s\S]*?)(?\n(?:Thought|Action|Observation)|\Z), text)一次性提取所有块再按顺序组装成结构化步骤列表。2. Tool Validator语义级守卫每个工具注册时必须提供ToolSpecPydantic Modelclass DBSearchSpec(BaseModel): query: str Field(..., descriptionSQL 查询语句禁止含 INSERT/UPDATE) timeout: int Field(30, ge1, le60, description超时秒数) class ToolRegistry: def __init__(self): self.tools: Dict[str, Tuple[Callable, Type[BaseModel]]] {} def register(self, name: str, func: Callable, spec: Type[BaseModel]): self.tools[name] (func, spec)当解析出Action: db_search时先查tools字典再用spec.parse_obj(action_input)做强校验。如果query字段含DROP TABLE直接抛ValidationError触发 fallback 逻辑如返回固定提示“检测到危险操作已拒绝执行”。3. State Guard状态级守卫在每次进入新节点前检查当前状态是否允许此动作。例如“生成报告”节点必须满足alarm_history和manual_result两个字段都存在且非空。这个检查放在LangGraph的conditional_edge函数里而不是在节点内部做确保状态流转的确定性。3.2 支柱二MCP 执行层——把工具调用变成基础设施能力我们没用 LangChain 的Tool抽象而是直接对接 MCP Server。整个执行层只有两个核心对象1. MCP Client轻量 HTTP 封装class MCPClient: def __init__(self, base_url: str): self.client httpx.AsyncClient(base_urlbase_url, timeout30.0) async def execute_tool(self, tool_name: str, params: dict) - dict: # 自动注入 trace_id 和 span_id用于全链路追踪 headers {X-Trace-ID: current_trace_id(), X-Span-ID: current_span_id()} response await self.client.post( f/tool/execute, json{tool: tool_name, params: params}, headersheaders ) if response.status_code 200: return response.json() # 返回标准 MCP 响应格式 else: raise MCPExecutionError(fTool {tool_name} failed: {response.text})2. MCP Server 配置YAML 驱动的工具注册mcp-tools.yaml文件示例tools: - name: query_alarm_history description: 查询设备历史告警记录 input_schema: type: object properties: device_id: type: string start_time: type: string format: date-time output_schema: type: array items: type: object properties: id: {type: string} code: {type: string} handler: src.tools.alarm.query_history - name: search_manual description: 在设备手册中搜索错误代码 input_schema: type: object properties: error_code: {type: string} manual_version: {type: string} handler: src.tools.manual.searchMCP Server 启动时自动加载此文件生成 OpenAPI 文档并将handler字符串动态导入为可调用对象。当业务方新增一个 OPC UA 工具时只需改 YAML重启 ServerAgent 代码零修改。实操心得MCP Server 必须实现熔断和降级。我们在handler执行前加了circuit_breaker装饰器连续3次超时后自动熔断5分钟期间所有调用直接返回{status: error, error: service_unavailable}。这比让 Agent 卡在超时里强十倍。3.3 支柱三状态管理层——让每一步都可追溯、可重放、可审计LangGraph 的PostgreSQLSaver是基础但我们在此之上加了三层增强1. 状态分片存储不把整个 state 对象塞进一个JSONB字段。而是按领域拆分state_core: 包含task_id,current_node,retry_count等元数据state_inputs: 存各节点输入用JSONB但字段名严格限定如alarm_query_params,manual_search_paramsstate_outputs: 存各节点输出同样结构化state_logs: 存纯文本日志流用于快速 grep这样做的好处是查某个任务的数据库查询参数直接SELECT alarm_query_params FROM state_inputs WHERE task_id xxx不用jsonb_extract_path_text(state, inputs, db_search)这种慢查询。2. 状态快照与 Diff每次状态更新不仅存当前值还存与上一版的diff用deepdiff库计算。当用户投诉“报告里设备型号写错了”运维可以直接查state_diffs表看到是哪一步把model_number从S120-4PT改成了S120-4P精准定位问题节点。3. 状态事件广播用 Kafka 发布StateUpdatedEvent包含task_id,node_name,statussuccess/failed/retry下游服务可订阅监控服务实时计算各节点平均耗时、失败率通知服务当status failed时自动发企业微信告警审计服务存入区块链存证我们用的是 Hyperledger Fabric3.4 支柱四可观测性层——让 Agent 不再是黑盒Agent 最怕的不是报错而是“静默失败”——LLM 一本正经胡说八道你还以为它干得挺好。我们用三招破局1. Token 级耗时埋点在 LLM 调用前后打点start time.time() response await llm.ainvoke(messages) end time.time() # 计算 prompt_tokens completion_tokens 的耗时 token_cost (end - start) / (len(prompt_tokens) len(completion_tokens)) # 如果 token_cost 0.1s/token触发慢查询告警 if token_cost 0.1: logger.warning(fSlow LLM call: {token_cost:.3f}s/token for {task_id})这个指标比单纯看total_time有用得多能暴露模型退化、网络抖动等问题。2. Action 执行黄金三指标对每个工具调用监控action_latency_ms: 从发出请求到收到响应的时间含网络action_error_rate: 错误响应占比HTTP 4xx/5xx 或 MCPstatus: erroraction_cache_hit_rate: 对于幂等查询如手册搜索加 Redis 缓存统计命中率我们发现query_alarm_history的error_rate突然从 0.1% 升到 5%查日志发现是 OPC UA 服务器证书过期——这根本不是 Agent 的问题但可观测性让我们提前 2 小时发现。3. 思维链Thought质量分析用另一个小模型Qwen1.5-0.5B对 LLM 的Thought字段做轻量分析是否包含明确的下一步动作关键词查、调、生成、对比是否引用了上一步的Observation检测Observation中的 ID/Code 是否出现在Thought里是否出现矛盾陈述如先说“F305 是过压”后又说“需检查欠压保护”每天聚合分析生成thought_quality_score。当分数低于阈值自动触发 prompt 优化流程——这才是真正的闭环。4. 实操全流程从本地调试到生产部署的 7 个关键节点4.1 节点一本地开发——Ollama LangChain MCP Server 三位一体本地环境我坚持“零云依赖”全部跑在笔记本上Ollama 拉取模型# 选 Qwen2.5-7B-Instruct中文强、推理快、显存友好 ollama pull qwen2.5:7b-instruct # 启动时指定 GPUMac M2 用 metalWindows 用 cuda ollama run qwen2.5:7b-instruct --gpu启动 MCP Server# 用官方 mcp-server-ollama pip install mcp-server-ollama mcp-server-ollama --model qwen2.5:7b-instruct \ --tools-config ./mcp-tools.yaml \ --host 0.0.0.0:8080LangChain Agent 代码最小启动from langgraph.graph import StateGraph from src.agents.protocol_guard import ReActParser from src.agents.mcp_client import MCPClient # 初始化 mcp_client MCPClient(http://localhost:8080) parser ReActParser() # 定义节点 def query_alarm_node(state): thought parser.parse_thought(state[messages][-1].content) action parser.parse_action(state[messages][-1].content) result await mcp_client.execute_tool(action.name, action.input) return {messages: [AIMessage(contentfObservation: {result})]} # 构建图 workflow StateGraph(AgentState) workflow.add_node(query_alarm, query_alarm_node) workflow.set_entry_point(query_alarm) app workflow.compile(checkpointerPostgresSaver())关键技巧本地调试时把MCPClient的timeout设为 10 秒避免卡死同时开启httpx的logging所有请求/响应明文打印比任何 debug 断点都直观。4.2 节点二Prompt 工程——不是写得越长越好而是让 LLM “不敢乱说”我们的 system prompt 只有 128 字但经过 17 轮 A/B 测试你是一个工业设备诊断专家严格遵守 ReAct 协议。只输出 Thought/Action/Action Input/Observation 四种格式。Action 名称必须从以下列表中选择[query_alarm_history, search_manual, generate_report]。禁止编造 Observation禁止省略 Action Input 字段。若无法确定输出 Thought: 需更多信息请用户提供设备型号和故障时间。重点在三个“禁止”禁止编造 Observation防止 LLM 胡诌手册内容曾发生过它把 F305 说成“电机堵转”实际是“直流母线过压”禁止省略 Action InputLangChain 的ReActOutputParser会把缺失Action Input的 Action 当作无效直接卡住禁止自由发挥所有 Action 名必须精确匹配大小写、下划线一个都不能错测试时我们用 200 条真实工单构造测试集对比不同 prompt 下的Action 解析成功率。当加入“禁止编造”后成功率从 82% 跃升至 99.3%——因为 LLM 学会了“宁可不行动也不乱行动”。4.3 节点三工具开发——用 Pydantic V2 写出“自带文档”的函数每个工具函数必须配一个InputSchema和OutputSchemafrom pydantic import BaseModel, Field from typing import List, Optional class QueryAlarmInput(BaseModel): device_id: str Field(..., patternr^PLC-\d{4}$, description设备ID格式如 PLC-8827) start_time: str Field(..., descriptionISO 8601 时间戳如 2024-07-12T15:00:00Z) limit: int Field(10, ge1, le100, description最多返回条数) class AlarmRecord(BaseModel): id: str code: str time: str severity: str # critical/warning/info class QueryAlarmOutput(BaseModel): records: List[AlarmRecord] total_count: int def query_alarm_history(input: QueryAlarmInput) - QueryAlarmOutput: # 实际数据库查询逻辑 ... return QueryAlarmOutput(recordsrecords, total_countlen(records))好处是三重输入强校验device_id不符合PLC-\d{4}直接 422自动生成 OpenAPIFastAPI 一键导出/openapi.json供 MCP Server 加载前端调用友好TypeScript 客户端可直接用zod解析类型安全注意Pydantic 的Field(description...)会被 LangChain 的StructuredTool.from_function自动提取为 tool description所以写好 description 就等于写好了 prompt 里的工具说明。4.4 节点四状态持久化——PostgreSQL 的 JSONB 优化实战state表结构设计是性能关键CREATE TABLE agent_state ( task_id VARCHAR(64) PRIMARY KEY, current_node VARCHAR(64) NOT NULL, retry_count INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), -- 分片存储避免大 JSONB 拖慢查询 core_data JSONB NOT NULL DEFAULT {}::jsonb, inputs_data JSONB NOT NULL DEFAULT {}::jsonb, outputs_data JSONB NOT NULL DEFAULT {}::jsonb, logs TEXT -- 大文本用 TEXT不走 JSONB ); -- 关键索引 CREATE INDEX idx_state_node ON agent_state(current_node); CREATE INDEX idx_state_updated ON agent_state(updated_at); -- 对 inputs_data 中的 device_id 建 GIN 索引加速按设备查 CREATE INDEX idx_inputs_device ON agent_state USING GIN ((inputs_data-device_id));实测对比当state平均大小 2MB 时单表JSONB存储的查询耗时 1200ms分片后降到 86ms。原因在于 PostgreSQL 的JSONB索引只对顶层键有效inputs_data-db_search-device_id这种路径无法索引必须拆到顶层字段。4.5 节点五CI/CD 流水线——让 Agent 变更像数据库迁移一样安全我们把 Agent 部署纳入 GitOps 流程变更即 PR所有mcp-tools.yaml修改、system_prompt更新、state表结构变更都走 GitHub PR自动化测试PR 触发流水线运行三类测试unit_test: 工具函数单元测试mock 数据库integration_test: 启动本地 Ollama MCP Server跑 50 条回归用例schema_migration_test: 用sqlalchemy检查state表结构变更是否向后兼容灰度发布新版本先路由 1% 流量监控action_error_rate和thought_quality_score达标后全自动全量最狠的一次system_prompt优化后thought_quality_score从 85 升到 92但action_error_rate意外升高 0.3%。流水线自动拦截回滚 PR工程师查了 3 小时才发现——新 prompt 里“请严格遵守协议”这句话让 LLM 在不确定时更倾向于输出Thought: 需更多信息而旧 prompt 是直接瞎猜。这就是数据驱动的价值。4.6 节点六生产监控——Grafana 里的 5 个生死攸关看板我们给 Agent 部署了 5 个核心看板每个都关联告警看板名称监控指标告警阈值说明节点健康度node_success_rate{nodequery_alarm} 95%连续5分钟低于阈值发企业微信LLM 响应质量llm_token_cost_ms_per_token{modelqwen2.5} 0.15s/token检测模型退化或 GPU 过载MCP 调用延迟mcp_action_latency_ms{actionsearch_manual}P95 2000ms手册搜索超时影响整体诊断时长状态存储压力pg_stat_database_blks_read{datnameagent_db}P99 10000磁盘 I/O 瓶颈预警思维链可信度thought_consistency_score{task_typediagnosis} 0.7LLM 开始自相矛盾需人工介入其中thought_consistency_score是我们自研指标用 Sentence-BERT 计算相邻两步Thought的余弦相似度再结合Observation引用率加权。当它跌穿 0.7基本意味着 LLM 已进入“胡言乱语”模式必须熔断。4.7 节点七故障排查——一份真实的线上事故复盘事故时间2024年7月15日 14:22现象37% 的诊断任务卡在search_manual节点超时后重试最终失败排查过程查 Grafanamcp_action_latency_ms{actionsearch_manual}P95 从 800ms 暴涨到 4200ms查 MCP Server 日志大量ConnectionResetError: [Errno 104] Connection reset by peer查网络MCP Server 与手册服务部署在另一 K8s 命名空间间 Calico 网络策略被误删根本原因运维执行集群升级时脚本错误清除了manual-service的 ingress 规则修复与改进立即恢复网络策略在 MCP Client 加retry_strategy对ConnectionResetError自动重试 2 次间隔 1s在search_manual工具里加本地缓存Redis缓存error_code - manual_section映射缓存失效时才走网络这次事故教会我Agent 的可靠性永远取决于它所依赖的最脆弱一环。再完美的 ReAct 协议也救不了网络中断。5. 常见问题与避坑指南那些文档里绝不会写的血泪经验5.1 “为什么我的 Function Calling 总是不触发”——90% 的问题出在这 3 个地方问题类型典型表现根本原因解决方案Prompt 注入失败LLM 完全忽略 tools只输出普通文本System prompt 里没写清楚“必须从以下工具中选择”或工具 description 太模糊在 system prompt 末尾加一句“你的 Action 名称必须精确匹配以下列表中的某一项包括大小写和下划线”参数格式不匹配LLM 输出{device_id: PLC-8827}但工具要求device_id: strLangChain 的StructuredTool默认用json.loads()解析不校验字段类型改用PydanticTool传入InputSchema让 Pydantic 做强类型转换和校验上下文长度溢出前几步正常到第5步突然不调用工具ReAct 协议要求把所有 History 塞进 promptHistory 越来越长挤占了留给 Action 的 token实现 History 截断策略只保留最近2轮Thought-Observation更早的用摘要代替如“已查PLC-8827告警共12条”实操心得用langchain_community.chat_models.ollama.Ollama时务必设置num_ctx8192或更高否则 Ollama 默认 2048ReAct History 很容易撑爆。5.2 “LangGraph 状态丢了怎么办”——4 种状态丢失场景及抢救方案场景如何发现紧急抢救长期预防进程崩溃服务日志出现Segmentation fault或Killed从 Kafkastate_eventsTopic 重放最近10条事件重建状态进程启动时先从 Checkpointer 加载最新状态再订阅 KafkaCheckpointer 故障SELECT COUNT(*) FROM state返回 0从备份库每日 pg_dump恢复state表启用 PostgreSQL 的wal_levellogical用 Debezium 同步 state 表到 Kafka实现双写状态被覆盖同一 task_id 的updated_at时间戳来回跳查state_logs找谁在并发修改同一 task_id在PostgreSQLSaver.put方法里加ON CONFLICT (task_id) DO UPDATE用EXCLUDED.updated_at做时间戳比较网络分区两个实例同时处理同一 task状态冲突人工介入根据logs判断哪边更完整手动合并引入分布式锁Redis Redlock获取lock:task_id后才执行状态更新5.3 “MCP Server 怎么调试”——3 个必用命令行技巧查看已注册工具curl http://localhost:8080/tools # 返回所有工具的 OpenAPI Spec确认你的工具在列表里手动触发工具调用绕过 Agentcurl -X POST http://localhost:8080/tool/execute \ -H Content-Type: application/json \ -d {tool: query_alarm_history, params: {device_id: PLC-8827, start_time: 2024-07-12T15:00:00Z}}这比在 Agent 里调试快 10 倍能快速定位是工具逻辑问题还是 Agent 解析问题。查看 MCP Server 日志级别# 启动时加 --log-level DEBUG mcp-server-ollama --log-level DEBUG --model qwen2.5:7b-instruct # 日志里会显示Received tool request, Validating params, Executing handler...5.4 “微信 AI Agent 怎么接入”——不是加个公众号而是重构消息协议很多团队想做“微信 AI Agent”以为接个公众号 Webhook 就行。错。微信消息是异步的而 Agent 是状态机必须解决会话绑定微信FromUserName是唯一标识但 Agent 的task_id是 UUID需建立映射表wechat_session_map(from_user, task_id, last_active)消息截断微信单条消息上限 2000 字Agent 输出可能超长。解决方案用textnews消息组合长文本分段发每段末尾加[继续]按钮状态同步用户在微信发“查PLC-8827”Agent 正在查数据库此时用户又发“算了”需能中断当前 task。我们在wechat_session_map加status字