☰
AI Agent长任务执行的工程化四大支柱
2026/10/26 13:14:45 网站建设 项目流程

1. 项目概述:这不是在调用一个API,而是在搭建一条自动化产线

“Agent工程篇:当 AI Agent 从单次回答走向长任务执行,工程工作到底发生在哪里?”——这个标题里藏着一个被多数人忽略的真相:我们过去三年谈的“AI Agent”,90%还停留在“Prompt工程师手搓一个能查天气+写周报的Demo”阶段;而真正进入工程化落地的Agent,本质是一套需要持续调度、状态管理、错误熔断、资源隔离、可观测性的异步任务系统。它和传统Web服务完全不同,不是“请求-响应”模型,而是“意图-规划-执行-反思-重试”的闭环生命周期。我带团队落地过6个跨部门Agent项目,从电商客服自动补单、到供应链异常协同处置、再到研发内部代码变更影响面分析,最深的体会是:决定项目成败的,从来不是大模型多强,而是工程层能不能把“不确定的LLM输出”稳稳地装进“确定的系统边界”里。你看到的“Agent自动完成采购审批”,背后是23个微服务接口的编排校验、7类超时策略的分级熔断、4层上下文缓存的生命周期管理、以及每次失败后自动生成的可读性诊断报告。这已经不是NLP范畴,而是典型的分布式系统工程问题。本文不讲LangChain怎么链Prompt,也不对比Dify和CrewAI哪个UI更漂亮,只聚焦一个硬核问题:当Agent要连续跑8小时、处理17个子任务、调用5类外部工具、中间经历3次模型幻觉修正——那些真正消耗工程师80%时间的“脏活累活”,具体落在哪几个技术模块?每个模块的取舍逻辑是什么?为什么有些团队花3周搭出不可维护的玩具,而另一些团队用2人月做出支撑日均20万次调用的Agent平台?这些答案,全藏在“长任务执行”这个关键词里。

2. 核心思路拆解:为什么单次调用和长任务执行是两种物种?

2.1 从“函数调用”到“状态机驱动”的范式迁移

很多人误以为Agent工程就是“把多个API调用串起来”,这是最大的认知陷阱。单次回答场景下(比如问“今天北京天气如何”),整个流程是线性的:用户输入 → Prompt组装 → LLM调用 → 结果解析 → 返回前端。整个过程无状态、无分支、无重试,耗时通常在2秒内完成。但长任务执行(比如“帮我分析Q3销售数据异常,定位到华东区A类SKU,生成整改建议并邮件同步给区域总监”)则完全不同:

  • 必须维持跨步骤的上下文一致性:第3步调用BI工具查出华东区数据后,第5步生成邮件时仍需准确引用该数据范围,不能因为中间插入了其他用户消息就丢失;
  • 必须处理非确定性输出:LLM可能在第2步把“华东区”错写成“华南区”,导致后续所有操作偏离目标,系统需自动识别偏差并触发修正循环;
  • 必须支持人工干预点:当Agent在第4步生成整改建议时,法务同事需要介入审核条款措辞,系统得暂停执行、保留现场状态、等待人工确认后再续跑;
  • 必须保障资源隔离:同一时刻有200个销售分析任务并发执行,某个任务因外部API超时卡死,不能拖垮其他任务的内存和线程。

这些需求直接推导出四个不可绕过的工程模块:状态持久化引擎、执行流控制器、异常熔断网关、人工干预通道。它们共同构成Agent长任务执行的“操作系统内核”。我见过太多团队在LangChain里堆砌RunnableSequence,结果跑两天就OOM,根本原因就是把本该由状态机管理的长期状态,强行塞进Python进程内存里——这就像试图用Excel表格管理一座核电站的实时工况数据。

2.2 工程重心的位移:从Prompt层下沉到基础设施层

搜索热词里高频出现“Prompt Engineering”“Context Engineering”,但实际落地中你会发现:当任务链超过5个步骤,Prompt优化带来的收益会急剧衰减,而基础设施缺陷造成的故障率却指数级上升。我们做过量化统计:在某电商售后Agent项目中,初期85%的失败源于LLM输出格式错误(如JSON少了个逗号),团队花2周优化System Prompt后,这类错误降到12%;但随之暴露的是新问题——剩余88%的失败中,63%来自外部API超时未熔断、19%来自上下文缓存过期导致步骤间数据错乱、7%来自并发任务互相污染内存。这意味着:工程工作的主战场,已经从“怎么让模型说对”转向“怎么让系统扛住说错”。具体表现为三个位移:

  1. 调试对象位移:从盯着LLM返回的JSON字段,变成盯着Prometheus监控里的agent_task_duration_seconds_bucket直方图、context_cache_hit_rate指标、tool_call_timeout_total计数器;
  2. 技术栈位移:从纯Python生态(LangChain/LlamaIndex)扩展到分布式协调(Redis Streams)、状态快照(PostgreSQL JSONB列)、异步任务队列(Celery/RabbitMQ)、服务网格(Istio流量治理);
  3. 交付物位移:从一份可运行的Notebook,变成一套带SLA承诺的K8s Operator(支持kubectl apply -f agent-deployment.yaml一键部署)、配套的OpenTelemetry追踪链路、以及面向运维的告警规则集(如“连续3次tool_call_failed且retry_count>2触发P1告警”)。

这种位移不是技术炫技,而是业务倒逼的结果。当你的Agent要自动处理供应商合同续签,涉及法务系统、ERP、电子签章平台三系统联动,任何环节的不可靠都会导致法律风险——这时候,比“让LLM写出完美条款”更重要的是:“当电子签章API返回503时,能否自动降级为邮件通知并记录审计日志”。

2.3 架构选型的底层逻辑:为什么Rust在Agent框架中突然崛起?

热词里反复出现“基于rust语言ai agent”,这不是偶然。当我们把长任务执行拆解为“状态管理+IO密集+高并发”三大特征时,Rust的内存安全+零成本抽象+异步运行时(Tokio)优势就凸显出来。举个真实案例:某金融风控Agent需每秒处理200+交易流水,对每个流水执行“反洗钱规则匹配→关联账户图谱查询→实时额度计算”三步。用Python实现时,GIL导致CPU利用率卡在30%,峰值延迟达8秒;改用Rust重写核心执行引擎后,CPU打满至95%,延迟稳定在350ms内。关键差异在于:

  • 状态共享安全:Python中多个异步任务共享context: dict极易引发竞态(如两个任务同时修改context['risk_score']),Rust通过Arc<RwLock<Context>>强制编译期检查,杜绝此类Bug;
  • IO等待零开销:Python的asyncio.sleep()本质是事件循环调度,而Rust的tokio::time::sleep()直接映射到epoll/kqueue,百万级连接下调度开销近乎为零;
  • 内存布局可控:Agent执行中大量使用Vec<ToolCall>、HashMap<String, Value>等结构,Rust可精确控制内存分配(如预分配Vec::with_capacity(128)),避免Python频繁GC导致的STW停顿。

当然,Rust不是银弹。我们团队采用“Python胶水层+Rust核心引擎”的混合架构:前端交互、知识库接入、低频管理API仍用Python(生态成熟),而状态机调度、工具调用编排、实时上下文同步等性能敏感模块用Rust实现,通过pyo3暴露Python接口。这种分层设计,既规避了Rust生态在LLM推理(如vLLM集成)上的短板,又抓住了其在系统工程领域的核心优势。

3. 核心模块详解:长任务执行的四大支柱

3.1 状态持久化引擎:让Agent拥有“记忆”而非“缓存”

长任务执行中最常被轻视的模块,却是故障率最高的环节。很多团队用in-memory cache或Redis SET存上下文,结果在任务执行到第7步时发现context['user_id']变成了None——因为Redis缓存过期了,或者某个中间步骤意外覆盖了键值。真正的状态引擎必须满足四个硬性要求:

  • 事务性更新:一次状态变更(如“记录BI查询结果+更新当前步骤=step_4”)必须原子提交,不能出现“结果存了但步骤没更新”的中间态;
  • 版本化快照:每次步骤跳转前自动保存状态快照,当任务因LLM幻觉失败时,能回滚到上一稳定版本(类似Git commit);
  • 生命周期绑定:状态存活时间必须与任务生命周期严格一致(如任务超时自动清理),不能依赖TTL这种粗粒度机制;
  • 结构化存储:避免用扁平key-value存复杂嵌套对象,应使用支持JSON路径查询的数据库(如PostgreSQL的JSONB + GIN索引)。

我们最终选择PostgreSQL作为主状态存储,原因很实在:

  1. ACID保障:用UPDATE context_state SET data = jsonb_set(data, '{steps,4}', '"completed"') WHERE task_id = 't-123' AND version = 7一条SQL完成原子更新,避免Redis事务的WATCH-EXEC复杂度;
  2. 快照低成本:每次更新前执行INSERT INTO context_snapshots SELECT task_id, version+1, data FROM context_state WHERE task_id = 't-123',利用PG的MVCC特性,快照存储几乎零额外开销;
  3. 调试友好:DBA可以直接SELECT>BEGIN; -- 检查版本号防止覆盖 SELECT version, data FROM context_state WHERE task_id = 't-123' AND version = 7 FOR UPDATE; -- 原子更新 UPDATE context_state SET data = jsonb_set( jsonb_set(data, '{steps,4, status}', '"running"'), '{steps,4, started_at}', to_jsonb(now()) ), version = 8, updated_at = now() WHERE task_id = 't-123' AND version = 7; -- 更新成功则提交,否则ROLLBACK COMMIT;

    这套设计使状态更新P99延迟稳定在8ms内,远低于LLM调用本身的网络延迟(平均320ms),完全不构成瓶颈。

    3.2 执行流控制器:让Agent学会“看路标”而非“蒙眼狂奔”

    LLM本身不具备流程控制能力,它只是按Prompt描述“尽力而为”。当任务链变长,必须有一个独立于LLM的控制器来确保执行不脱轨。我们称之为“Orchestrator”,它承担三项核心职责:

    • 步骤导航:根据当前状态和LLM输出,决定下一步执行哪个工具、传什么参数、是否需要人工确认;
    • 条件分支:解析LLM返回的{"decision": "need_approval", "reason": "contract_amount > 100000"},触发法务审批流程;
    • 循环管理:当LLM返回{"action": "retry", "step": "analyze_data"}时,自动加载该步骤快照并重试,而非从头开始。

    Orchestrator的实现关键是将LLM输出结构化为可执行指令。我们定义统一的ToolCallSchema:

    { "tool_name": "bi_query", "parameters": { "region": "east_china", "sku_category": "A" }, "next_step": "generate_report", "on_failure": { "retry": {"max_attempts": 3, "backoff": "exponential"}, "fallback": "notify_admin" } }

    Orchestrator收到此结构后,不再解析自然语言,而是直接调用对应工具。这带来质变:

    • 可测试性:能用单元测试覆盖所有on_failure分支,无需调用真实LLM;
    • 可观测性:每步执行前记录orchestrator_decision{tool="bi_query", step="step_3"}指标,快速定位决策瓶颈;
    • 可干预性:当next_step为"escalate_to_human"时,自动创建Jira工单并暂停任务。

    我们用Rust的tokio::sync::mpsc实现轻量级Orchestrator,核心逻辑仅200行代码:

    // 从状态库读取当前任务 let state = load_context(task_id).await?; // 解析LLM输出为ToolCall let tool_call = parse_llm_output(&state.data["llm_output"])?; // 执行工具调用(带熔断) let result = execute_tool_with_circuit_breaker(&tool_call).await?; // 更新状态并决定下一步 update_context(task_id, &state, &tool_call, &result).await?; // 发布下一步指令到消息队列 command_bus.send(NextStepCommand { task_id, next_step: tool_call.next_step }).await?;

    这种设计让Orchestrator成为纯粹的“交通警察”,不参与业务逻辑(如BI查询怎么写),只确保流程合规。当业务变化时(如新增“法务审核”步骤),只需修改ToolCallSchema和update_context逻辑,Orchestrator核心代码零改动。

    3.3 异常熔断网关:给每个外部调用装上“保险丝”

    长任务执行中,80%的故障来自外部依赖(API超时、数据库慢查询、第三方服务抖动)。如果放任LLM不断重试,轻则任务卡死,重则引发雪崩。我们的熔断网关采用三层防御:

    防御层级触发条件动作实例
    L1:单次调用熔断HTTP 5xx / 超时 > 3s立即返回CIRCUIT_OPEN,跳过本次调用调用电子签章API超时,直接降级为邮件通知
    L2:服务级熔断连续5次L1熔断将该服务标记为DEGRADED,后续请求走备用路径ERP系统不可用时,从缓存读取上一版库存数据
    L3:任务级熔断单任务累计10次L1熔断终止任务,触发人工介入流程合同分析任务反复失败,自动创建钉钉待办

    熔断状态存储在Redis中,用Hash结构实现:

    HSET circuit_state:erp_service status DEGRADED HSET circuit_state:erp_service last_failure "2024-06-15T14:22:33Z" HSET circuit_state:erp_service fallback_path "cache_inventory"

    关键创新在于熔断决策与LLM解耦。传统做法是让LLM在Prompt里写“如果ERP调用失败,尝试缓存”,这极不可靠。我们的方案是:Orchestrator在调用前先查Redis熔断状态,若为DEGRADED则直接跳过调用,从fallback_path读取数据,并将{"source": "fallback_cache", "data": {...}}注入上下文。LLM全程不知情,仍按原Prompt执行,但结果已自动兜底。

    注意:熔断阈值必须动态调整。我们用滑动窗口算法计算失败率:failed_count / total_count in last 60s。初始阈值设为50%,当服务恢复后,失败率自然下降,熔断器自动关闭。硬编码if failure_count > 5会导致服务波动时永久熔断。

    3.4 人工干预通道:让Agent懂得“该停时就停”

    再智能的Agent也无法处理所有边界情况。当LLM生成“建议终止与供应商X的合作”时,法务必须审核条款合法性;当销售分析Agent提出“下调华东区A类SKU价格20%”,CEO需要拍板。人工干预不是功能补充,而是长任务执行的必备环节。我们设计了三类干预点:

    • 前置审批点:任务启动前,对高风险操作(如资金转账、合同签署)强制人工确认;
    • 过程拦截点:执行中检测到LLM输出含敏感词(如“终止合作”、“罚款”),暂停并推送审批;
    • 后置复核点:任务完成后,对关键输出(如财务报表、法律意见)生成摘要,供人工抽检。

    技术实现上,我们用企业微信/钉钉机器人作为统一入口。当Orchestrator决定触发干预时:

    1. 将当前任务快照(含所有上下文、步骤日志、LLM原始输出)序列化为PDF;
    2. 调用钉钉API发送审批卡片,附带“同意”“驳回”“修改后重试”三个按钮;
    3. 用户点击后,回调URL接收操作结果,Orchestrator据此更新任务状态。

    关键细节在于状态冻结与解冻。干预期间,任务状态必须锁定,禁止任何自动执行。我们用PostgreSQL的SELECT ... FOR UPDATE实现:

    -- 获取任务锁 BEGIN; SELECT * FROM agent_tasks WHERE task_id = 't-123' AND status = 'waiting_approval' FOR UPDATE; -- 更新状态为approved,解锁 UPDATE agent_tasks SET status = 'approved', approver = 'zhangsan', approved_at = now() WHERE task_id = 't-123'; COMMIT;

    这套机制使人工干预平均耗时从12分钟降至3.2分钟(因PDF摘要精准定位关键信息),且0%出现“用户审批时Agent已自动执行完毕”的竞态问题。

    4. 实操全流程:从零搭建一个可落地的长任务Agent

    4.1 环境准备与依赖安装

    我们采用最小可行架构:Python 3.11(胶水层)+ Rust 1.75(核心引擎)+ PostgreSQL 15 + Redis 7。所有组件均通过Docker Compose一键拉起,避免环境差异导致的“在我机器上能跑”问题。

    docker-compose.yml核心配置:

    version: '3.8' services: # PostgreSQL状态库 db: image: postgres:15 environment: POSTGRES_DB: agent_state POSTGRES_USER: agent POSTGRES_PASSWORD: secure_password volumes: - ./pg-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U agent -d agent_state"] interval: 30s timeout: 10s retries: 5 # Redis熔断网关 redis: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 20s timeout: 10s retries: 3 # Rust核心引擎(编译后镜像) orchestrator: build: ./orchestrator environment: DATABASE_URL: "postgres://agent:secure_password@db:5432/agent_state" REDIS_URL: "redis://redis:6379" depends_on: - db - redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 15s

    Python胶水层依赖(requirements.txt):

    # LLM交互 openai==1.35.0 langchain-core==0.1.49 # 数据库连接 psycopg2-binary==2.9.9 redis==4.6.0 # 企业微信/钉钉集成 dingtalkchatbot==1.5.4

    Rust核心引擎Cargo.toml关键依赖:

    [dependencies] tokio = { version = "1.36", features = ["full"] } sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls"] } redis = "0.26" serde = { version = "1.0", features = ["derive"] } tracing = "0.1" opentelemetry = "0.22"

    实操心得:别用pip install psycopg2!在Docker中编译C扩展极不稳定。务必用psycopg2-binary,虽体积大10MB,但节省3小时环境调试时间。Rust侧用sqlx而非tokio-postgres,因其支持编译时SQL检查(sqlx::query("SELECT * FROM context_state").fetch_all(pool)),提前捕获表名错误。

    4.2 状态库初始化与Schema迁移

    PostgreSQL状态库需预建三张表,我们用sqlx migrate管理迁移:

    migrations/20240601000001_init.up.sql:

    -- 任务主表 CREATE TABLE agent_tasks ( task_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(64) NOT NULL, intent TEXT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL ); -- 上下文状态表(核心) CREATE TABLE context_state ( task_id UUID PRIMARY KEY REFERENCES agent_tasks(task_id) ON DELETE CASCADE, version BIGINT NOT NULL DEFAULT 0, data JSONB NOT NULL DEFAULT '{}', updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, CONSTRAINT context_expires_check CHECK (expires_at > updated_at) ); -- 创建GIN索引加速JSONB查询 CREATE INDEX idx_context_data_gin ON context_state USING GIN (data); -- 快照表 CREATE TABLE context_snapshots ( id SERIAL PRIMARY KEY, task_id UUID NOT NULL REFERENCES agent_tasks(task_id), version BIGINT NOT NULL, data JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (task_id, version) );

    执行迁移命令:

    # Python端执行 sqlx migrate run -d postgres://agent:secure_password@localhost:5432/agent_state # Rust端(用于测试) sqlx migrate run -d "postgres://agent:secure_password@db:5432/agent_state"

    注意:gen_random_uuid()需启用pgcrypto扩展。首次启动容器后,手动执行:

    CREATE EXTENSION IF NOT EXISTS "pgcrypto";

    否则task_id默认值失效,导致插入失败。这个坑我们踩了两次,第一次以为是ORM配置问题,花了半天查Python代码。

    4.3 Orchestration核心逻辑实现(Rust)

    Rust核心引擎orchestrator/src/main.rs实现四步闭环:

    Step 1:任务启动与状态初始化

    #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?; // 监听任务队列(RabbitMQ) let mut conn = amqp::Connection::connect("amqp://guest:guest@rabbitmq:5672").await?; let channel = conn.create_channel().await?; let queue = channel.queue_declare("agent_tasks", Default::default()).await?; // 消费任务 let mut consumer = channel.basic_consume( &queue, "orchestrator_consumer", BasicConsumeOptions::default(), ).await?; while let Some(delivery) = consumer.recv().await? { let task_payload: TaskPayload = serde_json::from_slice(&delivery.data)?; // 初始化状态 let initial_context = json!({ "user_input": task_payload.user_input, "steps": [], "memory": {}, "created_at": chrono::Utc::now().to_rfc3339() }); sqlx::query( "INSERT INTO context_state (task_id, version, data, expires_at) VALUES ($1, 0, $2, $3)" ) .bind(&task_payload.task_id) .bind(&initial_context.to_string()) .bind(task_payload.expires_at) .execute(&pool) .await?; delivery.ack(BasicAckOptions::default()).await?; } Ok(()) }

    Step 2:LLM输出解析与工具调用

    // 定义ToolCall结构体 #[derive(Deserialize)] struct ToolCall { tool_name: String, parameters: HashMap<String, Value>, next_step: String, on_failure: FailurePolicy, } #[derive(Deserialize)] struct FailurePolicy { retry: RetryConfig, fallback: String, } // 解析LLM输出(简化版) fn parse_llm_output(output: &str) -> Result<ToolCall, ParseError> { // 实际使用regex提取JSON块,避免LLM在文本中混入无关内容 let json_match = regex::Regex::new(r"\{.*?\}").unwrap(); let json_str = json_match.find(output).map(|m| m.as_str()).unwrap_or("{}"); serde_json::from_str(json_str) }

    Step 3:带熔断的工具执行

    async fn execute_tool_with_circuit_breaker( tool_call: &ToolCall, pool: &PgPool, ) -> Result<ToolResult, ExecutionError> { // 查询熔断状态 let mut redis_conn = redis::Client::open(std::env::var("REDIS_URL")?)? .get_async_connection().await?; let circuit_state: Option<String> = redis::cmd("HGET") .arg(format!("circuit_state:{}", tool_call.tool_name)) .arg("status") .query_async(&mut redis_conn) .await?; match circuit_state.as_deref() { Some("DEGRADED") => { // 执行fallback路径 execute_fallback(&tool_call.fallback, &tool_call.parameters).await } _ => { // 正常执行 let result = match tool_call.tool_name.as_str() { "bi_query" => bi_query_tool(&tool_call.parameters).await?, "send_email" => send_email_tool(&tool_call.parameters).await?, _ => return Err(ExecutionError::UnknownTool), }; // 记录成功指标 metrics::counter!("tool_call_success_total", "tool" => &tool_call.tool_name); Ok(result) } } }

    Step 4:状态更新与下一步分发

    async fn update_context( task_id: &Uuid, current_state: &ContextState, tool_call: &ToolCall, result: &ToolResult, pool: &PgPool, ) -> Result<(), sqlx::Error> { // 生成新版本号 let new_version = current_state.version + 1; // 构建新上下文 let mut new_data = current_state.data.clone(); new_data["steps"].push(json!({ "step_id": format!("step_{}", new_version), "tool": &tool_call.tool_name, "parameters": &tool_call.parameters, "result": result, "timestamp": chrono::Utc::now().to_rfc3339() })); // 原子更新 sqlx::query( "UPDATE context_state SET data = $1, version = $2, updated_at = NOW(), expires_at = $3 WHERE task_id = $4 AND version = $5" ) .bind(&new_data.to_string()) .bind(new_version) .bind(current_state.expires_at) .bind(task_id) .bind(current_state.version) .execute(pool) .await?; Ok(()) }

    4.4 Python胶水层集成LLM与Orchestrator

    Python层负责LLM调用、用户交互、以及与Rust引擎的HTTP通信。关键在于将LLM的不确定性转化为Orchestrator可执行的确定性指令。

    LLM调用封装(llm_client.py):

    import openai from pydantic import BaseModel, Field from typing import List, Optional class ToolCallResponse(BaseModel): """强制LLM输出结构化ToolCall""" tool_name: str = Field(..., description="要调用的工具名,如bi_query") parameters: dict = Field(..., description="工具参数字典") next_step: str = Field(..., description="下一步骤名") on_failure: dict = Field(..., description="失败策略") def call_llm_for_tool_selection( user_input: str, context: dict, available_tools: List[str] ) -> ToolCallResponse: system_prompt = f""" 你是一个Agent执行规划器。根据用户输入和当前上下文,选择一个最合适的工具执行。 可用工具:{available_tools} 输出必须是严格JSON,包含字段: - tool_name: 字符串,必须是可用工具之一 - parameters: 对象,包含该工具所需参数 - next_step: 字符串,下一步骤名 - on_failure: 对象,含retry和fallback字段 不要输出任何解释性文字,只输出JSON。 """ response = openai.beta.chat.completions.parse( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, {"role": "assistant", "content": json.dumps(context)} ], response_format=ToolCallResponse ) return response.choices[0].message.parsed

    与Rust Orchestrator通信(orchestrator_client.py):

    import requests import json class OrchestratorClient: def __init__(self, base_url: str = "http://orchestrator:8080"): self.base_url = base_url def start_task(self, user_id: str, intent: str, expires_in_seconds: int = 7200): """启动新任务""" payload = { "user_id": user_id, "intent": intent, "expires_in_seconds": expires_in_seconds } resp = requests.post(f"{self.base_url}/tasks", json=payload) resp.raise_for_status() return resp.json()["task_id"] def get_next_action(self, task_id: str) -> dict: """获取下一步执行指令""" resp = requests.get(f"{self.base_url}/tasks/{task_id}/next") resp.raise_for_status() return resp.json() def submit_result(self, task_id: str, result: dict): """提交工具执行结果""" resp = requests.post( f"{self.base_url}/tasks/{task_id}/result", json={"result": result} ) resp.raise_for_status() # 使用示例 client = OrchestratorClient() task_id = client.start_task("u-123", "分析华东区A类SKU销售异常") while True: action = client.get_next_action(task_id) if action["status"] == "completed": print("任务完成:", action["output"]) break # 调用LLM生成ToolCall tool_call = call_llm_for_tool_selection( user_input=action["intent"], context=action["context"], available_tools=["bi_query", "send_email", "notify_slack"] ) # 执行工具(此处调用真实API) result = execute_tool(tool_call.tool_name, tool_call.parameters) # 提交结果 client.submit_result(task_id, result)

    这套胶水层设计实现了关注点分离:Python专注LLM交互与业务语义,Rust专注状态与流程,两者通过REST API解耦。当需要升级LLM(如从GPT-4切换到Claude-3.5),只需修改Python层,Rust引擎完全不受影响。

    5. 常见问题与避坑指南:那些没人告诉你的血泪教训

    5.1 “LLM输出JSON格式错误”问题的根治方案

    几乎所有团队都遭遇过:LLM返回{"tool_name":"bi_query", "parameters":{...}(缺右括号)导致JSON解析失败。常规做法是加try-catch重试,但这治标不治本。我们的根治方案分三层:

    1. Prompt层加固:在System Prompt末尾强制添加:

      IMPORTANT: Your output MUST be valid JSON without any explanation. End your response with a single closing curly brace }.
    2. 解析层容错:用json5库替代json,支持注释、单引号、尾逗号等宽松语法:

      import json5 try: parsed = json5.loads(llm_output) except Exception as e: # 尝试修复常见错误 fixed = llm_output.rstrip(',') + '}' parsed = json5.loads(fixed)
    3. 架构层兜底:当解析失败次数>3,Orchestrator自动触发“LLM校准模式”——将原始输出和错误信息喂给另一个轻量LLM(如Phi-3),指令为:“请将以下文本修复为合法JSON,只输出JSON,不要任何解释:{llm_output}”。实测将解析失败率从

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询