Blog / Deer404
Agent 的执行控制如何设计
本地代码助手的基本流程并不复杂:用户给出任务,模型决定读哪些文件、修改什么,再调用工具执行命令,根据结果继续。读文件和命令行工具接通后,已经能做出可用的演示。
这里结合 nano-agent 和 agent_demo 两个本地项目讨论。nano-agent 中的任务调度、会话状态和命令授权,用来决定动作何时可以执行;agent_demo 中的访问策略、命令运行器和上下文整理,则展示了进程启动前后需要处理的事情。两者是独立的原型,下面会分别说明它们的做法与限制。
可以先看一个简单任务:修改一份配置,然后运行检查。模型能够提出这两个动作,但程序还需要保证检查不会在修改完成前开始,用户拒绝的命令不会运行,超时后也不会在旧进程仍工作时又执行一遍。
这些要求决定了执行程序不能只是把模型给出的字符串交给 shell。它需要保存任务状态,确定本次动作的内容,管理进程,并根据实际结果决定后续动作。提示词能帮助模型安排工作,运行中的约束仍要落实到代码里。
从工具调用扩展到任务调度
一次只做一个动作时,可以把结果交回模型,让它决定下一步。这个办法适合短任务。步骤多了,或者执行中断后需要继续,仅靠对话里的一句“配置已经改好”,就很难知道检查到底能否开始。
nano-agent 的调度器把步骤写成带依赖的清单。执行前先检查任务 ID 是否重复、依赖是否存在、有没有依赖环;执行时根据前置任务的状态判断哪些任务可以启动。任务进入运行状态以后,才允许报告完成。
对“修改配置,再运行检查”来说,检查依赖的是修改任务的完成状态。顺序因此可以由程序检查,不需要每一轮都重新要求模型记住。但完成状态必须由执行器如实提交:写文件失败却报告成功,调度器仍会放行后续检查。状态机只能约束收到的信息,不能代替工具核实文件究竟改了什么。
这里可以把三个结果分开理解:动作获准,说明权限检查通过;进程成功退出,说明这次命令返回了成功状态;用户目标完成,还需要查看修改与验证结果。格式化命令退出码为零,并不能说明功能修改正确。
同一会话的并发也会影响顺序。nano-agent 在会话内记录当前操作,忙时直接返回错误,结束或抛错后都释放标记。也可以选择排队,不过排队中的动作在开始前仍要重新检查条件,等待期间文件可能已经变化。现有标记只保存在进程内的会话对象里,多个进程操作同一个工作区时,还需要另外的协调方式。
授权需要对应确定的动作
调度器允许开始,不代表动作已经获得用户许可。运行测试可能是正常步骤,最终要执行的命令、参数和目录仍需确定。同名脚本在不同目录下可能做不同的事,批准之后再改参数,也可能改变原先的操作范围。
nano-agent 的授权流程会遍历计划中的 shell 步骤,解析命令,查询会话或持久许可。用户拒绝时停止本轮授权,选择持久许可则保存供以后使用。这样能减少重复确认,但复用条件也成为权限逻辑的一部分。
当前缓存按解析出的命令字符串查询,解析抛错的分支则跳过该项检查。因此这一层还没有做到未知输入默认拒绝,也不能仅由缓存命中推断参数和目录都受过相同审查。
要继续完善,可以先固定一次动作的命令、参数、工作目录和声明的访问范围,让授权与执行读取同一份记录。内容变化后重新判断许可,而不是批准一份描述、执行时又临时拼出另一条命令。这个共同动作记录是后续设计方向,两个原型还没有统一实现。
许可的有效期也需要写清楚。仅限本次、会话内复用、持久复用,影响的范围不同。把这些差异留在运行程序里,才能在恢复任务或切换工作目录后重新判断,不能指望模型一直记住某次口头同意的边界。
把访问限制接到进程上
命令通过授权以后,才轮到运行环境。检查脚本内部还可能读取其他文件、访问网络或启动子进程。用户同意运行脚本,并没有自动让程序具备限制这些行为的能力。
agent_demo 的访问策略模块会检查调用方声明的读路径、写路径和域名,区分策略拒绝与能力不可用,再返回准备结果。但准备后的命令和参数仍原样交给子进程运行器,在这两层中没有看到操作系统隔离器接入启动路径。
这说明已有实现能判断“声明的目标是否符合策略”,还不能证明真实进程只访问了这些目标。即使结果中写着“已强制执行”,也要继续检查进程是怎样启动的。能力不可用时允许继续运行,与在受限环境中执行,给出的保证也不同。
如果要补上隔离,就需要把约束接到进程创建处,并用真实越界操作验证。可以让测试命令尝试读取范围外的文件、访问未允许的地址,再观察是否被阻止;它启动的子进程也要覆盖。名单函数测试能验证策略判断,不能替代这些运行检查。
对于只读工具和能修改工作区的脚本,产品可以选择不同限制。但需要先说明允许执行到什么程度,再决定哪些能力缺失时必须停止。一个写路径列表本身还不足以承诺任意命令都能安全运行。
命令的结束与结果接纳
接着看配置检查已经启动的情况。假设命令先生成了一份文件,再进入耗时检查,最后超时。调用方停止等待时,文件可能已经写好,进程也可能尚未退出。
agent_demo 的命令运行器到期先发 SIGTERM,随后安排一次延迟的 SIGKILL 尝试,后一次信号取决于进程状态判断。它记录超时标志、退出码、信号、耗时和输出,但没有逐一证明子进程、孙进程和相关资源已经全部回收。
因此超时不能直接转换成“重新执行”。恢复前要先核对已经发生的修改和仍在运行的任务,才能决定重试检查,还是重新执行整个动作。否则第一次命令已经产生副作用,第二次又从头开始,可能重复写入。这个运行器保留了部分运行信息,完整的恢复策略仍需继续实现。
输出也是单独的资源问题。现有运行器把标准输出和标准错误完整读成文本,没有大小上限。运行时间短的命令也可能输出大量内容,所以设置超时并不会同时限制内存使用。后续可以改为流式收集、限制保留大小并记录截断事实,再按需要保存完整输出的引用。
对于上层任务,最终接纳的结果应能区分正常退出、执行失败和超时,并保留核查依据。即使当前目标只是运行检查,也不要让执行器把所有异常都转成一段普通输出,否则调度器和模型会失去判断任务能否继续所需的信息。
对话上下文与执行记录
工具结果还会回到模型。任务执行得越久,日志、文件内容和历史对话越多,下一轮输入也越大。直接丢掉旧消息,可能让一个工具结果失去对应的调用;全部保留,又会不断增加输入。
agent_demo 在消息数量或字符数超过阈值后,把旧消息整理成摘要,再接近期消息。截取位置遇到工具结果时,会向前保留相关调用,外部记忆也限制了条数与单条字符数。
不过,这里的摘要主要是格式化拼接,没有保证压缩比例。触发整理的阈值也不是最终输入长度上限,系统提示、记忆和近期消息仍要合计,字符数还不能直接当作 token 数。需要限制整体输入时,应在组装完成后继续核算,而不能只检查近期消息有几条。
它目前把旧对话摘要和外部记忆放进系统消息包装。包装方式不会改变来源的可信程度,读到的文件和工具输出中的指令,仍然只是输入数据,不能因此取得新的执行权限。授权检查要留在执行路径中,避免一次上下文整理改变了动作权限。
任务恢复也不宜完全依赖这份摘要。已经写入哪些文件、检查是否完成、下次从哪一步开始,更适合保存在独立执行记录里。摘要负责帮助模型理解当前情况,恢复则依据记录与实际状态核对。相关记录方式见任务中断恢复笔记,大型审查输入的分配见差异分块笔记。
一次动作的完整过程
把“修改配置,再运行检查”放回整条流程,可以得到下面的设计顺序。这是根据前面的职责整理出的流程,不表示两个原型已经共同实现了它:
确定任务依赖
→ 固定本次动作的命令、参数和目录
→ 判断许可与运行条件
→ 在约定环境中启动进程
→ 收集退出状态、输出和实际修改
→ 核对完成条件,更新执行记录
→ 继续下一步,或保留失败与恢复位置
每个阶段需要的验证不同。依赖与状态转换可以独立测试,授权内容是否等于最终执行内容需要集成检查,隔离和进程回收则必须放到真实运行环境中观察。某个模块测试通过,不能直接代替整条流程的保证。
这些职责已经在两个项目中有了对应实现,也各自留下了缺口:nano-agent 的授权流程有跳过解析异常的分支;agent_demo 的访问策略还没形成真实隔离,超时与上下文整理也没有覆盖全部资源限制。继续开发时,可以针对需要承诺的行为补齐对应部分。对于能写文件的助手,至少要先知道每个动作何时获准、实际做过什么,以及中断以后该凭什么继续。