尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AI Agent状态管理:用Harness替代LLM直接文件操作,实现可靠进度跟踪

AI Agent状态管理:用Harness替代LLM直接文件操作,实现可靠进度跟踪 1. 为什么“让LLM写文件”是个坏主意最近在搞AI Agent项目特别是涉及到需要长期运行、有状态、并且需要跟踪进度的任务时我发现一个非常普遍且危险的“捷径”直接把文件I/O操作交给LLM大语言模型去处理。比如让Agent在完成任务的过程中把中间结果、日志、或者最终报告写到一个本地文件里。听起来很自然对吧Agent需要“记忆”需要“输出”文件系统不就是最现成的存储介质吗但我要告诉你这几乎是给自己埋下了一颗定时炸弹。我见过太多这样的代码片段用LangChain或者类似框架在Tool里定义一个write_to_file函数然后告诉LLM“嘿你可以用这个工具把内容保存下来”。LLM也很“听话”在需要的时候就会调用这个工具。问题出在哪出在不可控性和状态管理的彻底失控上。LLM是一个基于概率生成文本的模型它的输出具有不确定性。你无法精确预测它会在什么时候、以什么样的格式、调用多少次文件写入操作。想象一下这个场景你的Agent正在处理一个多步骤任务比如“分析市场数据并生成周报”。LLM可能在第一步就尝试写入一个标题在第三步写入部分分析但因为网络抖动或自身生成长度限制它可能重复调用写入或者写入不完整、格式错误的片段。更糟糕的是如果任务失败或需要重试你怎么清理这些半成品文件如果多个Agent实例并发运行文件锁和写入冲突会让你瞬间陷入调试地狱。这不仅仅是文件内容混乱的问题更深层的是进度跟踪的失准。如果你的“进度”体现在生成的文件里那么一个被意外覆盖的文件、一个只有一半内容的文件都会让你的监控系统误判任务状态。你失去了对任务生命周期的清晰视图。所以标题里的“别让LLM写文件”核心是别把具有副作用的、持久化的状态操作直接暴露给非确定性的LLM。我们需要一个更可靠、更结构化、更可控的机制来管理Agent的状态和输出这就是“Harness”的价值所在。2. 理解Harness不是框架是缰绳与鞍具“Harness”这个词在中文里常被翻译成“线束”或“马具”在软件工程里它指的是一套用于控制、管理和测试复杂系统的工具和约定。在AI Agent的语境下特别是结合“进度跟踪”这个需求我们可以把Harness理解为一套赋予LLM可控执行环境的“缰绳与鞍具”。它不是另一个像LangChain或LlamaIndex那样的“框架”。框架提供的是构建块和设计模式而Harness关注的是运行时Runtime的控制。它的核心职责是隔离环境为LLM驱动的Agent提供一个安全的沙箱限制其能执行的操作比如不允许直接访问文件系统或数据库。状态管理集中、可靠地管理任务的状态、上下文和进度而不是让状态散落在LLM的对话历史或临时文件中。副作用管理将所有对外部世界产生影响的操作写文件、发API请求、更新数据库抽象为明确的、可审计的、可回滚的“动作”Action并由Harness统一调度和执行。进度可观测提供标准化的钩子hooks和接口让外部系统能够清晰地了解任务当前处于哪个阶段、完成了百分之几、产生了什么中间结果。为什么TypeScript/JavaScript生态在这件事上特别有优势因为现代前端和Node.js在状态管理如Redux、XState、响应式编程RxJS、以及异步流程控制Promise async/await方面有极其深厚的积累。这些理念正是构建一个强大Harness所需要的。用TypeScript来写Harness你能获得完善的类型安全确保LLM的输入输出、工具调用的参数、状态的结构都是明确定义的从开发阶段就避免了一大类运行时错误。一个常见的误解是Harness和Agent是并列的组件。实际上它们的层级关系更像是LLM 是引擎提供认知和决策能力Agent 是驾驶舱封装了目标、记忆和工具调用逻辑而 Harness 是底盘、传动系统和仪表盘提供安全的执行环境、状态传递和进度反馈。一个设计良好的Harness能让同一个Agent在不同的任务和环境中都稳定工作。3. 设计进度跟踪的核心状态机与上下文快照既然不让LLM直接碰文件那进度和状态存哪里怎么跟踪答案是用一个中心化的、结构化的状态对象并通过状态机State Machine来驱动其演进。3.1 定义任务状态结构首先我们需要为每个Agent任务定义一个强类型的状态接口。这个状态对象就是任务的“唯一真相来源”Single Source of Truth。// 使用TypeScript定义任务状态 interface AgentTaskState { taskId: string; status: pending | running | paused | completed | failed; progress: number; // 0到100的进度百分比 currentStep: string; // 当前步骤描述如 “正在搜集数据” context: { // 任务所需的输入参数 goal: string; parameters: Recordstring, any; // 任务执行中产生的中间数据 collectedData?: any[]; analysisResult?: any; // 最终输出不直接是文件路径而是结构化数据 finalOutput?: { reportTitle: string; sections: Array{heading: string; content: string}; summary: string; }; }; history: Array{ timestamp: Date; action: string; // 如 “call_tool: web_search”, “llm_reasoning” input?: any; output?: any; step: string; }; // 完整的操作历史用于审计和调试 error?: { message: string; stack?: string; step: string; }; }这个AgentTaskState对象包含了任务的一切。进度progress、当前步骤currentStep直接是字段而最终的报告内容也以结构化数据finalOutput的形式存在内存中与具体的存储介质解耦。3.2 实现状态机驱动接下来我们需要一个状态机来管理status的变迁。我们可以使用XState这样的库或者自己实现一个简单的版本。状态机确保了状态变迁是符合逻辑的、可预测的。class AgentTaskHarness { private state: AgentTaskState; private stateListeners: Array(state: AgentTaskState) void []; constructor(initialGoal: string) { this.state { taskId: task_${Date.now()}, status: pending, progress: 0, currentStep: 初始化, context: { goal: initialGoal, parameters: {} }, history: [{ timestamp: new Date(), action: task_created, step: 初始化 }], }; } // 更新状态的核心方法所有状态变更必须通过此方法 private updateState(updater: (prevState: AgentTaskState) AgentTaskState) { const oldStatus this.state.status; this.state updater(this.state); const newStatus this.state.status; // 状态机逻辑例如不能从 completed 跳回 running if (oldStatus completed newStatus running) { throw new Error(Illegal state transition: completed - running); } // 这里可以加入更多状态转移规则... // 通知所有监听者状态已更新 this.stateListeners.forEach(listener listener(this.state)); // 关键自动持久化状态快照例如到数据库或内存缓存 this.persistStateSnapshot(); } // 暴露给Agent调用的“安全”方法 async startTask() { this.updateState(prev ({ ...prev, status: running, currentStep: 开始执行任务, progress: 5, })); // 这里会开始执行真正的Agent逻辑... } async recordStep(stepName: string, action: string, result?: any) { this.updateState(prev { const newProgress Math.min(prev.progress 10, 95); // 示例进度计算 return { ...prev, progress: newProgress, currentStep: stepName, history: [ ...prev.history, { timestamp: new Date(), action, input: stepName, output: result, step: stepName }, ], }; }); } async completeTask(finalOutput: any) { this.updateState(prev ({ ...prev, status: completed, progress: 100, currentStep: 任务完成, context: { ...prev.context, finalOutput }, history: [ ...prev.history, { timestamp: new Date(), action: task_completed, step: 任务完成 }, ], })); // 触发后续处理如将 finalOutput 写入文件、发送通知等 await this.onTaskCompleted(finalOutput); } private async persistStateSnapshot() { // 将状态保存到外部存储如Redis、数据库而不是本地文件。 // 这是Harness的核心状态管理是集中式的、可靠的。 // await database.save(agent_tasks, this.state.taskId, this.state); console.log([Harness] State snapshot persisted for ${this.state.taskId}); } private async onTaskCompleted(finalOutput: any) { // 只有在这里在任务确定成功完成后才执行副作用操作如写文件。 // 这保证了文件的完整性和正确性。 const reportPath ./reports/${this.state.taskId}.json; await fs.writeFile(reportPath, JSON.stringify(finalOutput, null, 2)); console.log([Harness] Final report written to ${reportPath}); } // 允许外部订阅状态变化用于UI进度条、监控系统 subscribe(listener: (state: AgentTaskState) void) { this.stateListeners.push(listener); } }通过这个设计Agent的核心逻辑LLM的推理、工具调用不再直接接触文件系统。它只通过Harness提供的recordStep、completeTask等安全接口来更新进度和提交结果。所有的持久化操作无论是状态快照还是最终输出文件都由Harness在合适的时机、以可控的方式完成。这就好比飞行员Agent只负责操纵飞机和下达指令而飞机的飞控计算机Harness负责安全地执行这些指令并记录所有飞行数据。4. 实战构建一个带进度跟踪的文本摘要Agent Harness让我们用一个具体的例子来串联以上概念。假设我们要构建一个Agent其任务是“读取指定URL的文章并生成一份摘要报告”。我们将实现一个完整的Harness来管理它。4.1 定义工具与安全边界首先定义Agent可以使用的工具。注意我们不会提供writeFile工具。// tools.ts import axios from axios; import * as cheerio from cheerio; // 工具函数定义 const tools { async fetchWebPage(url: string): Promisestring { const response await axios.get(url); return response.data; }, async extractMainContent(html: string): Promisestring { const $ cheerio.load(html); // 简单的正文提取逻辑实际项目会更复杂 return $(article).text() || $(body).text(); }, async callLLMForSummary(text: string, instruction: string): Promisestring { // 模拟调用LLM API例如OpenAI // const response await openai.chat.completions.create({...}); // return response.choices[0].message.content; return 这是对文本的摘要基于指令${instruction}; } }; // 将工具封装成Agent可安全调用的接口 export class SafeToolDispatcher { constructor(private harness: AgentTaskHarness) {} async dispatch(toolName: keyof typeof tools, args: any) { // 1. 记录工具调用开始 await this.harness.recordStep(tool_call:${toolName}, 调用工具: ${toolName}, args); try { // 2. 执行工具 // ts-ignore const result await tools[toolName](...args); // 3. 记录成功结果 await this.harness.recordStep(tool_success:${toolName}, 工具调用成功: ${toolName}, result); return result; } catch (error) { // 4. 记录失败并更新任务状态为失败 await this.harness.recordStep(tool_failed:${toolName}, 工具调用失败: ${toolName}, error.message); // Harness可以决定是否重试或直接失败任务 throw error; } } }4.2 实现Agent核心逻辑与Harness集成现在我们实现Agent的主循环。它从Harness获取状态决定下一步行动并通过SafeToolDispatcher调用工具。// agent.ts export class SummarizationAgent { constructor( private harness: AgentTaskHarness, private tools: SafeToolDispatcher ) {} async run() { const state this.harness.getState(); // 假设Harness有getState方法 const { goal } state.context; // 第一步解析目标获取URL await this.harness.recordStep(parsing_goal, 解析任务目标); // 这里可以集成一个LLM来解析自然语言目标例如“总结 https://example.com 的文章” const url this.extractUrlFromGoal(goal); // 简化的解析函数 // 第二步抓取网页 await this.harness.recordStep(fetching_page, 抓取目标网页内容); const html await this.tools.dispatch(fetchWebPage, [url]); // 第三步提取正文 await this.harness.recordStep(extracting_content, 提取网页正文); const mainText await this.tools.dispatch(extractMainContent, [html]); // 第四步生成摘要 await this.harness.recordStep(generating_summary, 使用LLM生成摘要); const summary await this.tools.dispatch(callLLMForSummary, [mainText, 生成一份简洁的摘要]); // 第五步构建最终输出结构并提交给Harness await this.harness.recordStep(formatting_output, 格式化最终输出); const finalOutput { originalUrl: url, summary: summary, length: mainText.length, generatedAt: new Date().toISOString(), }; // 完成任务Harness会负责后续的持久化操作。 await this.harness.completeTask(finalOutput); } private extractUrlFromGoal(goal: string): string { // 简单的URL提取实际应用中可能需要更复杂的NLP或正则 const urlMatch goal.match(/https?:\/\/[^\s]/); if (!urlMatch) throw new Error(未在任务目标中找到有效URL); return urlMatch[0]; } }4.3 组装与运行完整的Harness流程最后我们创建一个主入口文件将Harness、工具分发器和Agent组装起来并展示如何跟踪进度。// main.ts import { AgentTaskHarness } from ./harness; import { SafeToolDispatcher } from ./tools; import { SummarizationAgent } from ./agent; async function main() { const taskGoal 请总结一下 https://news.example.com/tech-article 这篇文章的主要内容。; // 1. 创建Harness缰绳 const harness new AgentTaskHarness(taskGoal); // 2. 创建安全工具分发器 const toolDispatcher new SafeToolDispatcher(harness); // 3. 创建Agent骑手并注入Harness和工具 const agent new SummarizationAgent(harness, toolDispatcher); // 4. 订阅状态变化实现进度跟踪仪表盘 harness.subscribe((state) { console.log([进度更新] 任务ID: ${state.taskId}); console.log( 状态: ${state.status}); console.log( 进度: ${state.progress}%); console.log( 当前步骤: ${state.currentStep}); console.log(---); // 这里可以轻松地将状态推送到WebSocket、更新数据库、或刷新UI进度条 }); // 5. 启动任务 console.log(开始执行任务: ${taskGoal}); await harness.startTask(); try { await agent.run(); console.log(任务执行成功); } catch (error) { console.error(任务执行失败:, error); // Harness内部已经记录了错误状态这里可以进行统一错误处理如发送警报 } // 6. 任务结束后可以从Harness获取最终状态和输出 const finalState harness.getState(); if (finalState.status completed) { console.log(最终输出已由Harness处理可能已写入文件或数据库。); // 可以从finalState.context.finalOutput获取结构化结果 } } main();通过这个流程我们实现了进度可视化通过subscribe方法任何监听者都能实时获取任务进度。状态持久化persistStateSnapshot方法确保任务状态不会因进程重启而丢失。副作用控制写文件操作被隔离在onTaskCompleted中只有任务成功完成才会执行。错误恢复由于所有状态和操作历史都被记录如果任务中途失败我们可以分析state.history精准定位问题甚至设计从某个步骤重试的逻辑。5. 高级模式分布式、持久化与监控集成上面的例子是一个单进程的Harness。在实际生产环境中我们可能需要更强大的能力。5.1 分布式状态存储persistStateSnapshot方法不应该只是console.log。我们需要将其持久化到一个共享存储中以支持多机、多进程的Agent部署。// 使用Redis作为分布式状态存储 import { createClient } from redis; class DistributedHarness extends AgentTaskHarness { private redisClient; constructor(initialGoal: string) { super(initialGoal); this.redisClient createClient(); this.redisClient.connect(); } private async persistStateSnapshot() { const key agent:task:${this.state.taskId}; // 将状态序列化存储并设置过期时间 await this.redisClient.setEx(key, 3600, JSON.stringify(this.state)); // 1小时过期 } async loadState(taskId: string): PromiseAgentTaskState | null { const data await this.redisClient.get(agent:task:${taskId}); return data ? JSON.parse(data) : null; } }这样即使运行Agent的服务器重启我们也可以从Redis中恢复任务状态实现任务的断点续做。5.2 与工作流引擎如Dify集成Dify、LangGraph等工具擅长编排复杂的工作流。Harness可以与它们协同工作。你可以将Harness视为工作流中每个“LLM节点”或“工具节点”的执行器和状态管理器。例如在Dify中定义一个工作流其中一个节点是“摘要生成Agent”。当工作流执行到这个节点时它并不直接调用LLM而是创建一个AgentTaskHarness实例并传入参数。启动这个Harness并订阅其状态。等待Harness的状态变为completed或failed。从Harness的最终状态中取出finalOutput作为该节点的输出传递给工作流的下一个节点。这样Dify工作流负责宏观的业务流程编排而Harness负责微观的、单个Agent任务的可靠执行与进度跟踪两者各司其职。5.3 实现细粒度进度反馈与取消操作有时任务步骤很多10%一跳的进度过于粗糙。我们可以改进进度计算逻辑使其更精确。class PreciseProgressHarness extends AgentTaskHarness { private totalSteps: number; private currentStepIndex: number; constructor(initialGoal: string, totalSteps: number) { super(initialGoal); this.totalSteps totalSteps; this.currentStepIndex 0; } async recordStep(stepName: string, action: string, result?: any) { this.currentStepIndex; const newProgress Math.min(Math.floor((this.currentStepIndex / this.totalSteps) * 100), 100); this.updateState(prev ({ ...prev, progress: newProgress, currentStep: ${stepName} (${this.currentStepIndex}/${this.totalSteps}), history: [...prev.history, { timestamp: new Date(), action, step: stepName }], })); } }同时Harness应该支持任务的取消或暂停这对于处理长时间运行的任务或响应用户中断至关重要。class CancellableHarness extends AgentTaskHarness { private isCancelled false; cancel() { this.isCancelled true; this.updateState(prev ({ ...prev, status: paused, // 或 cancelled currentStep: 任务已被取消, })); } // 在Agent的每个步骤开始前检查 async safeRecordStep(stepName: string, action: string, fn: () Promiseany) { if (this.isCancelled) { throw new Error(Task was cancelled); } await this.recordStep(stepName, 开始: ${action}); const result await fn(); await this.recordStep(stepName, 完成: ${action}, result); return result; } }在Agent的run方法中将每一步操作包裹在safeRecordStep里就能实现可中断的任务。6. 避坑指南Harness设计中的常见陷阱在实际构建和使用Harness的过程中我踩过不少坑这里分享几个关键的注意事项。陷阱一状态对象过于庞大或频繁持久化如果AgentTaskState中的context或history字段不断增长例如存储了完整的网页HTML每次updateState都进行全量持久化如写入数据库会带来巨大的性能开销。解决方案是区分全量状态和增量快照。可以只持久化状态的核心元数据taskId,status,progress,currentStep而将大的context数据存储到对象存储如S3或专门的文档数据库在状态中只保留其引用。陷阱二工具调用的副作用未被完全隔离即使工具通过SafeToolDispatcher调用也要确保工具函数本身是“纯”的或者其副作用是受控的。例如一个“发送邮件”的工具在测试环境中不应该真的发邮件。Harness应该提供一个“模式”mode参数在测试模式下所有具有真实副作用的工具调用都被模拟mocked或记录而不执行。陷阱三进度计算逻辑与真实工作量脱节简单的步数进度currentStepIndex / totalSteps可能不准确特别是当每个步骤耗时差异很大时。一个更好的方法是允许每个步骤在recordStep时报告其自身的“权重”或预估耗时Harness根据权重来计算总体进度。或者更高级的做法是集成基于历史数据的预测。陷阱四忽略了并发和锁的问题当多个进程或线程可能同时操作同一个任务的状态时例如一个手动重试触发同时自动重试机制也在运行直接读写状态对象会导致竞态条件。在DistributedHarness中使用Redis的WATCH/MULTI/EXEC命令来实现乐观锁或者使用数据库的行锁确保状态更新的原子性。陷阱五Harness本身成为单点故障如果Harness的逻辑和状态存储紧密耦合一旦状态存储如Redis挂掉所有任务都会停滞。设计上Harness应该能优雅地处理后端存储的暂时不可用例如将状态缓存在内存中并不断重试持久化操作同时记录警告日志。任务的执行逻辑不应因为一次持久化失败而中断。构建一个健壮的Agent Harness其复杂性和重要性不亚于设计Agent本身。它要求开发者以系统工程的思维去考虑可靠性、可观测性和可维护性。当你成功地将LLM这匹“烈马”套上Harness这根“缰绳”后你才能放心地让它去驰骋完成更复杂、更长期的任务而你能始终清晰地知道它在哪里、在做什么、以及做得怎么样了。
返回列表