1. 为什么要把 Agent 从“一次性会话”变成“常驻服务”——从知识库问答的痛点说起
1.1 会话一断就失忆:Agent 的“临时工”本质
最近这段时间,我一直在折腾 WeKnora 的本地部署。WeKnora 本身是一个面向知识库场景的 RAG 项目,把文档、网页、数据库里的内容切块、向量化、建索引,然后基于检索结果做问答。跑通了基础链路之后,我的想法就比较“贪心”了:不满足于“问一句、答一句”的被动式问答,想让 Agent 主动去查询、分析、总结,甚至定期生成报告——比如每天早上自动巡检一遍知识库里新增的文档,把重点内容摘要推送到工作群。
但真正把 Agent 挂上去之后,第一个让我抓狂的问题出现了:Agent 本质上是个“临时工”。你用 Dify 或者其他 Agent 框架起一个会话,它确实能调用工具、多轮对话,但只要会话窗口关掉、浏览器刷新、或者服务重启,这个 Agent 的“记忆”就全部清零。它记不住你前天让它跟踪某个数据指标,记不住它上次搜索到一半的结论,更记不住用户偏好和上下文状态。你只能重新把背景资料再喂一遍,让它从零开始。
这背后的原因是:大多数 Agent 框架天然就是**无状态(stateless)**的。每一轮请求都是独立的,模型只拿到当前上下文窗口里塞进去的内容。上下文窗口一关,一切归零。短期跑个 Demo 没问题,长期用在生产环境里,根本没法接受。
1.2 WeKnora 本地部署后的真实瓶颈
我本地部署 WeKnora 的方式是 Docker Compose 起一套服务,包括向量库、文件解析服务、嵌入模型服务和后端 API。跑起来之后,知识库检索确实能用,但在上面叠加 Agent 能力时,我遇到了三个具体瓶颈:
第一个瓶颈是嵌入模型生命周期管理。WeKnora 的嵌入服务是随容器启动的,但 Agent 如果不常驻,它每次调用嵌入接口都要重新“热”一次模型,首 token 延迟非常高。更夸张的是,如果 Agent 是多轮对话里反复检索,嵌入模型的并发请求稍微一多,容器内存就告急。
第二个瓶颈是上下文窗口的物理限制。Agent 要做多轮对话、要调用工具、要引用检索片段,这些内容全部叠加之后,很快就把模型的上下文窗口塞满了。窗口满了怎么办?要么截断,要么报错,要么简单粗暴地丢给用户说“超出长度限制”。这显然不是一个“持久化”该有的体验。
第三个瓶颈是任务中断不可恢复。Agent 在执行一个长任务,比如批量分析几十个文档并生成摘要,执行到一半进程崩了、网络抖了、或者某个工具返回了格式异常的内容,整个任务就废了。没有持久化状态,就没有断点续跑。这跟数据库事务一个道理——没有“提交”和“重放”,你就永远不敢让流程跑太久。
1.3 持久化运行环境到底要解决哪三件事
为了把这些问题一次性解决干净,我决定基于 CubeSandbox 搭建一套独立的Agent 持久化运行环境,配合 WeKnora 做知识底座。这套环境的目标非常明确,就三件事:
| 要解决的问题 | 现状痛点 | 持久化环境的目标 |
|---|---|---|
| 对话记忆 | 会话关了就忘,上下文归零 | 每次交互落盘,下次启动可恢复全量上下文 |
| 任务状态 | 长任务中断即失败,无重试能力 | 任务状态持久化,支持断点续跑和失败重试 |
| 进程常驻 | 服务随用随启,模型反复加载 | Agent 以常驻服务形式运行,资源不浪费 |
这三个目标,基本就是“把 Agent 从临时工变成正式员工”的过程。接下来我在 CubeSandbox 里做的所有架构设计和代码实现,都是围绕这三件事展开的。
2. CubeSandbox 环境三层架构:会话持久层、执行沙箱、记忆总线
2.1 为什么叫“沙箱”而不是简单起一个常驻进程
最早我想过偷懒的方案:直接 nohup 起一个 Python 脚本常驻,所有状态存内存里,完事。但后来放弃了——原因很现实:Agent 要调用工具、执行命令、读写文件,如果没有任何隔离和限制,一个 bug 可能导致它删库跑路、写坏系统文件、或者把磁盘打满。
CubeSandbox 这个名字里的“Sandbox”指的是:给 Agent 一个可隔离、可限制、可回滚的运行边界,但同时在边界内又给它足够的自由度去干活。我从设计上把整套环境拆成三层:
- 会话持久层:负责所有交互数据的落盘和读取,是 Agent 的“日记本”。
- 执行沙箱:负责 Agent 的任务调度、工具调用、状态流转,是 Agent 的“车间”。
- 记忆总线:负责短期上下文与长期记忆的整理、压缩、检索,是 Agent 的“大脑皮层”。
三层之间通过标准 JSON 消息传递,不搞内部私有的 RPC 协议,这样后续想替换任意一层的实现都很方便。
2.2 会话持久层:把每一次交互都落盘
会话持久层我选择 SQLite 作为核心存储。SQLite 单文件、零运维、事务可靠,配合 WAL 模式之后并发读写能力对于个人或小团队的使用场景完全够用。
我设计的消息表结构大概是这样:
CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'tool', 'system')), content TEXT NOT NULL, tool_call_id TEXT, tool_name TEXT, extra JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, token_count INTEGER DEFAULT 0 ); CREATE INDEX idx_messages_session_id ON messages (session_id); CREATE INDEX idx_messages_created_at ON messages (created_at);这里有个细节:我把role分成了四种——user、assistant、tool、system。为什么要单独拆出tool?因为工具调用的结果是 Agent 推理链条里非常重要的一部分。如果只记 user 和 assistant,那工具返回了什么、报错原因是什么,事后排查全靠猜。把 tool 消息单独存下来,回放时能完整还原“Agent 当时看到了什么、为什么做出这个判断”。
extra字段我用了 JSONB(SQLite 里就是 JSON 文本,但我做了序列化约定),里面记录工具返回的截断摘要、耗时、模型名称、温度参数等元信息。这样既保留了原始记录的可追溯性,又不会让主表字段无限膨胀。
2.3 执行沙箱与任务队列:Agent 的“状态机”
Agent 光有记忆还不够,它必须能持续完成任务。执行沙箱的核心是一个任务状态机。我定义了以下状态:
idle -> thinking -> tool_calling -> responding -> idle |-> paused -> retry -> tool_calling |-> failed -> dead状态流转的数据也持久化到 SQLite,这样进程重启之后,沙箱能从paused或failed状态恢复,而不是直接丢弃任务。
任务队列我用的是一个简单的轮询机制:有一个tasks表,任务进入后标记为pending,沙箱里的调度器每隔几秒拉取待处理任务,逐个放入工作协程执行。没有引入 Redis 或者 MQ,因为对于个人知识库场景,引入额外中间件只会增加运维复杂度。当任务量真的上来了,把tasks表换成 Redis Stream 也是平滑迁移的。
这里有一个很关键的取舍:为什么不直接用 Dify 内部的任务队列?因为 Dify 的任务生命周期是跟随它的 Web 会话的,一旦客户端断开,Dify 端的任务也会被取消。而 CubeSandbox 的任务队列是独立运行在服务端的,客户端只负责提交任务和查询状态,任务的生死不由客户端决定。这样才能实现真正的“提交后即忘,服务端跑完”。
2.4 记忆总线:短期上下文与长期档案的中间层
持久化不等于把所有历史一股脑塞回上下文。如果每次对话都把之前所有消息送进模型,上下文窗口迟早爆炸。
记忆总线解决的问题是:如何把“过去发生了什么”变成模型真正能用得上的“记忆”。我的做法是分级处理:
- 短期记忆:最近的 N 轮对话原文,保存在内存里,同时持久化到 SQLite。N 一般取 10~20 轮。
- 中期摘要:每经过几轮对话,或者 token 数累计超过阈值,就调用 LLM 生成一份摘要,存入记忆库。
- 长期档案:定期把摘要压缩成一份“用户偏好与服务档案”,关联到具体的 session_id 或 user_id。下次会话启动时,只加载档案摘要,不加载全部历史。
这套机制很像人的记忆:你记得昨天吃什么(短期),记得上个月去旅行的大致安排(中期摘要),记得某些长期偏好(长期档案),但不会记得三个月前某个下午的每句对话。
3. 裸机到容器化的环境落地:数据目录、SQLite、对象存储和进程守护
3.1 目录设计:让数据目录成为 Agent 的“心脏”
持久化环境最忌讳的就是数据散落各处。我设计了一套清晰的目录结构,所有状态都集中在一个固定根目录下管理:
/opt/cubesandbox/ ├── data/ │ ├── sqlite/ │ │ ├── cubesandbox.db │ │ └── weknora_link.db │ ├── artifacts/ # Agent 生成的中间产物 │ │ ├── reports/ │ │ └── downloads/ │ └── logs/ ├── models/ │ └── embedding_cache/ # 嵌入模型本地缓存 ├── skills/ │ ├── search_weknora/ │ ├── doc_summary/ │ └── report_sender/ └── runtime/ └── supervisor.conf这里我把data/artifacts单独分出来,是为了防止 Agent 写文件时把数据散到系统的临时目录。所有 Agent 产生的文件必须落在 artifacts 下,这样清理、备份、权限控制都只需操作这一个目录。
3.2 SQLite 并发写入与 WAL 模式配置
很多人用 SQLite 最头疼的问题就是并发写入报database is locked。Agent 跑起来之后,多个任务同时写消息、写任务状态,锁冲突几乎是必然的。我的解决方案是两件事。
第一,开启 WAL 模式。WAL(Write-Ahead Logging)允许读操作与写操作并发进行,不再因为一个读事务阻塞整个写流程。连接 SQLite 时,我第一时间执行:
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;synchronous=NORMAL在 WAL 模式下能保证不会丢失数据,但比 FULL 模式的 fsync 频率低很多,写入吞吐能提升不少。busy_timeout=5000则是告诉 SQLite:遇到锁冲突时等 5 秒,而不是立刻报错。这两项配置一开,并发写入的报错概率大幅下降。
第二,写操作串行化。即使开了 WAL,多个写事务同时提交仍可能冲突。我在应用层做了一个简单处理:所有数据库写入操作统一走一个单 Goroutine(或者单线程)的 writer 通道,读操作可以随时并发。这样既保留 SQLite 的优势,又不至于为了并发写引入复杂的分布式事务。
3.3 进程守护:supervisor 配置和优雅关闭
Agent 常驻运行,进程守护是标配。我在 CubeSandbox 的 runtime 目录下放了一个 supervisor 配置:
[program:cubesandbox] command=/usr/bin/python3 -m cubesandbox.server directory=/opt/cubesandbox autostart=true autorestart=true startsecs=10 startretries=5 stopasgroup=true killasgroup=true stopwaitsecs=30 redirect_stderr=true stdout_logfile=/opt/cubesandbox/data/logs/cubesandbox.log注意stopasgroup=true和killasgroup=true这两个参数。因为 Agent 通常会派生子进程跑工具脚本,如果你只杀掉主进程,子进程可能变成孤儿继续跑,任务状态就会处于不一致状态。加上这两个参数,supervisor 会以进程组为单位做停止操作,保证“要么全停,要么全活”。
优雅关闭的具体策略是在主进程里监听 SIGTERM 信号,收到信号后先把当前正在执行的任务标记为paused,再把内存里的消息队列刷盘,最后才退出。这样下次启动时,所有未完成任务都能从paused状态继续,而不是从零开始。
3.4 容器化参数备忘:CPU/内存限制、重启策略、健康检查
如果你想把 CubeSandbox 也容器化,我强烈建议不要放裸机上裸跑。我把整个环境打包成 Docker 镜像后,docker-compose 的核心配置是这样的:
services: cubesandbox: build: . container_name: cubesandbox restart: unless-stopped volumes: - /opt/cubesandbox/data:/opt/cubesandbox/data - /opt/cubesandbox/models:/opt/cubesandbox/models deploy: resources: limits: cpus: "2.0" memory: 4g reservations: cpus: "0.5" memory: 1g healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9200/healthz"] interval: 30s timeout: 10s retries: 3 start_period: 40s有两个容易忽略的细节。
一是模型缓存目录一定要挂载到宿主机。嵌入模型第一次加载时会把权重文件写到缓存目录,如果不挂载,容器重建后模型要重新下载,几十 GB 的流量就白跑了,启动时间也会变成灾难级。
二是健康检查的start_period要设置得足够长。容器启动时需要加载模型、初始化 SQLite、恢复任务队列,这些操作非常耗时。如果 start_period 太短,健康检查会在服务还没就绪时就判定失败,导致容器频繁重启,进入恶性循环。我这边实测,单机部署 WeKnora 加 CubeSandbox 全链路的首次冷启动耗时约 40~60 秒,所以 40 秒起步的 start_period 是合理的。
4. 让 Agent 具备“超长上下文”的 RAG 记忆整合:从向量库到 Skills 注册表
4.1 向量库的接入方式:摆脱“塞满上下文”的笨办法
之前提到,Agent 在做知识库问答时,最笨的办法就是“把相关文档全塞进上下文”。CubeSandbox 的做法是集成 WeKnora 已有的向量检索接口,只把检索结果里最相关的 Top-K 片段送入上下文。
具体流程是:Agent 收到用户问题之后,先做一次查询改写(query rewriting),把模糊的意图转成更精确的检索词,然后调用 WeKnora 的检索接口获取 Top-K 片段。每个片段除了文本内容,还带上来源文档的元数据(文档名、页码、更新时间)。Agent 会根据这些片段生成回答,并在回答里给出引用来源。
这里有一个不错的实践:检索片段不直接填入模型上下文,而是先做一次压缩。每个检索片段可能长几百到上千字,Top-K 全部塞进去的话,5 个片段就是几千 token。我增加了一层compact_retrieval的预处理,用一个小模型把每个片段压缩成“摘要 + 关键事实 + 引用编号”的结构。经过压缩后,同样的信息量可能只要原来 1/3 的 token。
4.2 记忆摘要的生成机制:分层摘要
记忆摘要这块,我设计了一个“触发 + 生成 + 归档”的流程:
- 触发条件:当消息表的 token 累计值超过 8000(大概相当于 6000 字),或者距离上次生成摘要超过 30 分钟,就触发一次摘要任务。
- 生成方式:调用 LLM,把最近的 20 轮消息提炼成结构化摘要,包括:用户的核心诉求、已经完成的事项、未完成的事项、关键数据点、Agent 当时的决策原因。
- 归档策略:摘要写入
summaries表,关联 session_id。当摘要数量超过 5 份时,再把多份摘要合并成一份“长期档案”,只保留档案本身做全量回溯。
这套分层策略跑起来之后,我给 Agent 开了一个连续一周的测试任务,让它每天分析知识库新增内容并输出日报。一周之后,它还能准确说出“周三那份报告里提到的某个数字”,靠的就是长期档案里的关键数据点,而不是把所有原始消息都翻一遍。
4.3 Skills 注册表:把工具和技能变成可检索可编排的积木
Agent 光有记忆是不够的,它还需要“会干活”。我在 CubeSandbox 中设计了一个Skills 注册表,用来管理 Agent 可以使用的所有技能和工具。
每个技能注册时包含四个部分:
name:技能唯一名称,如search_weknoradescription:描述这个技能在什么情况下使用,越详细越好,因为这决定了模型会不会调用它parameters:JSON Schema 格式的参数定义,模型按这个格式生成调用参数permissions:这个技能允许访问的资源级别,比如readonly、write_artifacts、network
注册表以 JSON 文件形式存储在skills/目录下。Agent 启动时,注册表会被加载进上下文,模型在选择工具时就能看到所有可用的技能和它们的功能说明。
这里有个经验:技能描述一定要写清楚使用边界。比如search_weknora,我最初的描述写的是“搜索知识库”,结果模型经常在用户只是闲聊时也去搜索。后来我把描述改成“当用户需要查询文档内容、查找数据指标、回溯历史信息时使用;日常寒暄、总结观点时不使用”。加上这个限定之后,误调用率明显下降。
4.4 对话持久化之后的再检索:给 Agent“翻日记”
持久化的最大价值在于:Agent 能检索自己的历史。用户问“我之前是不是问过类似的问题”“上周你说过什么结论”,Agent 不需要瞎编,它可以把它自己的记忆库当作一个迷你知识库来检索。
我在记忆总线里为摘要和关键消息建了向量索引(直接复用 WeKnora 的嵌入服务),当收到涉及历史回顾的意图时,Agent 会先检索自己的记忆库,拿到相关摘要之后再进行回复。这一步让 Agent 真正有了“跨会话的连续性”,也是我整个环境里最满意的一个设计。
5. 接入 WeKnora 的 OIDC、多身份认证与安全边界
5.1 OIDC 配置:让 Agent 的调用方都经过认证
Agent 的连接和上线,安全性是第一位的,尤其是本地部署了一个能持久的智能体时,必须把控好访问权限。我在 CubeSandbox 与 WeKnora 的对接层中配置了 OIDC(OpenID Connect)认证,让所有请求 Broker 的调用方都先经过身份校验。
具体配置时,我先在 WeKnora 后端的管理端生成一个客户端凭据,然后修改 CubeSandbox 的配置文件:
oidc: issuer_url: "https://your-weknora-domain/oidc" client_id: "cubesandbox" client_secret: "your-secret" scopes: - openid - profile - email配置完成后,CubeSandbox 启动时会在后台通过 OIDC 发现机制获取公钥,所有 User Session 和 API 请求都会附带 Bearer Token。Broker 内部调用 WeKnora 的检索接口时,还会携带这个 Token 去换取短期访问凭证,整体上做到了进出双向鉴权。
有一点要特别注意:OIDC 的密钥文件不要写进 Docker 镜像或仓库。我一开始为了方便,把 client_secret 直接写进了环境变量文件,后来排查时发现在镜像构建历史里都明文记录了。应该改为从宿主机的secrets/目录挂载注入,或者在启动脚本里从加密文件中读取。
5.2 沙箱边界:文件系统、网络白名单、命令白名单
安全边界的核心是“最小权限”。我在执行沙箱里设置了三层限制:
- 文件系统限制:Agent 只能读写
/opt/cubesandbox/data/artifacts/目录,其他目录一律拒绝。用 Python 的os.path.realpath做前缀校验,哪怕传../../etc/passwd也会被拦截。 - 网络白名单:Agent 需要访问 WeKnora 检索接口、嵌入模型服务、以及用户配置的 Webhook。我把这些地址列入白名单,其他外网地址一律拒绝。这样可以避免 Agent 被恶意提示词诱导去访问内网或外部不明站点。
- 命令白名单:部分技能需要执行系统命令,比如
ffmpeg转码、pandoc转格式。我维护了一个命令白名单,只允许执行白名单内的命令,且参数做正则校验。不是有资格执行任意命令的万能 shell。
列一个简单的白名单配置示例:
{ "allowed_filesystem_root": "/opt/cubesandbox/data/artifacts", "allowed_networks": ["127.0.0.1:9200", "10.10.10.5:8080"], "allowed_commands": [ {"cmd": "ffmpeg", "args_pattern": "^-[i]\\s+.+\\.(mp4|mkv)"}, {"cmd": "pandoc", "args_pattern": "^[a-zA-Z0-9_/.-]+\\.md$"} ] }这三层限制看起来很简单,但实际执行中拦下了很多“计划外”的行为。有一次我测试时故意构造了一个提示词,诱导 Agent “查看系统环境变量”,结果被文件系统限制拦下了,日志里留下了一条清晰的审计记录。
5.3 审计日志:记录每次敏感操作
启用认证和权限控制之后,还需要一份“听话”的审计日志。我在沙箱层打了几类事件:CALL_TOOL、TOOL_RESULT、AUTH_FAILED、PERMISSION_DENIED、TASK_STARTED、TASK_FINISHED。每次事件的原始消息、耗时、涉及目标、最终结果,都写入 SQLite 的audit_log表。
为什么要打审计日志?因为 Agent 会自主执行一系列操作,这些操作可能包含数据读取、文件生成、消息推送。没有审计日志,出了问题你连“Agent 当时干了什么”都说不清。有了审计,每次事故都能反向追溯,定位是哪一步决策触发了异常行为。
6. 实测效果、性能参数和踩坑记录
6.1 一周运行结果:会话恢复、记忆回调、任务成功率
把整套环境跑起来之后,我用一套真实场景压测了整整一周:WeKnora 本地知识库包含 300 多篇公司内部文档,Agent 每天做一次定时巡检,分析新增文档并生成日报,同时响应用户的临时问答请求。
这一周的实测数据如下:
| 指标 | 结果 |
|---|---|
| 总对话轮次 | 1856 |
| 定时任务执行次数 | 7(每天一次) |
| 任务成功率 | 99.3%(一次失败因外部 API 超时) |
| 会话恢复率 | 100%(重启后所有上下文可恢复) |
| 平均响应延迟 | 2.8 秒(不含模型生成时间) |
| 上下文溢出次数 | 0(记忆压缩生效) |
其中最让我惊喜的不是响应速度,而是会话恢复率。我中途因为系统更新重启过一次 CubeSandbox,重启之后 Agent 能准确提到“上次分析到第 218 号文档,其中发现有 3 篇与服务器运维相关”,这种跨进程、跨会话的连续性,正是持久化运行环境的核心价值。
6.2 踩坑一:SQLite 的锁等待风暴
刚上线第一天我就踩了一个大坑。任务队列调度器每隔几秒扫描一次任务表,同时多个 Agent 会话在并发写消息表,结果不到半小时,日志里全是database is locked。
我当时的第一反应是“SQLite 不适合这种场景,换 PostgreSQL 吧”,但冷静下来分析后发现,问题的根因是我在应用层没有做写操作串行化。多个协程同时 UPDATE 同一条记录,再多的 WAL 配置也顶不住。于是我在代码里加了一个全局的写锁(mutex),所有写库操作排队执行,锁冲突瞬间消失。 SQLite 作为持久化核心继续用到现在,没有换数据库。
6.3 踩坑二:embedding 模型撑不住并发请求
第二个坑出在嵌入模型服务上。WeKnora 的嵌入模型服务默认是单 worker 的,Agent 同时在执行“文档巡检 + 在线问答 + 记忆检索”三个任务时,瞬间并发十几个嵌入请求,模型服务直接 OOM 崩溃。
解决方法是两方面的:一是给嵌入服务容器加内存上限(从 2GB 调整到 4GB),并配置了模型量化参数;二是在 CubeSandbox 的检索模块里加了简单的信号量限制,并发嵌入请求数控制在 4 个以内,多余的请求耐心排队。这样模型服务再也没有崩过。
6.4 踩坑三:进程被 OOM killer 干掉之后
第三个坑是宿主机层面的。有一次 Agent 处理一个超大 PDF 时,文件解析服务内存暴涨,触发 Linux 的 OOM killer,直接杀掉了 CubeSandbox 进程。当时 supervisor 自动重启了进程,但由于我没配置优雅关闭,进程是被强杀的,部分任务状态停留在tool_calling,恢复后出现了一个“幽灵任务”卡在队列里,调度器一直尝试重放,发现状态已经是running,就永远不处理了。
解决办法是双管齐下:把 agent 任务状态机的所有状态转换改为“乐观锁 + 超时恢复”。具体来说,每条任务记录有一个updated_at字段,调度器发现某条任务处于running状态但超过 300 秒没有更新,就强制把状态改回pending并重新调度。这样即使进程被强杀,重启后也能自动清理“僵死任务”。
7. 写在最后:关于 Agent 持久化的几点个人体会
整套 CubeSandbox 的搭建过程,最大的感受是:持久化不是把历史存下来那么简单,它是一整套围绕“状态管理”的系统工程。会话持久层解决记忆,执行沙箱解决任务连续性,记忆总线解决上下文窗口限制,Skills 注册表解决工具编排,OIDC 和沙箱边界解决安全,审计日志解决可追溯性——每一层都不可或缺。
如果你也在做 Agent 应用,我建议先从最小的闭环起步:消息表落盘、任务状态持久化、摘要记忆,这三件事做扎实了,Agent 的体验会有一个质的飞跃。至于更复杂的编排、多 Agent 协作、动态技能加载,都是在这个地基上慢慢长出来的。
最后再分享一个小技巧:所有的会话 ID、任务 ID 在持久化层一定要用结构化的前缀(比如sess_xxxx、task_xxxx),加上创建时间的复合索引。前期看似多余,但等你的 Agent 跑上几千轮、几千个任务之后,查询和排查的效率会完全不一样。这套环境我目前还在持续迭代,后续如果有新的心得,再来分享。