1. 本地编排为什么比全托管调度更靠谱
1.1 我曾经踩过的“多Agent”假协作
先说一个让我下决心换方案的场景。之前做某个信息抽取项目时,我尝试在同一段系统提示词里塞三套角色定义:一段负责整理原文,一段负责打标签,一段负责生成总结。结果跑起来的表现很分裂——模型经常把后面角色的输出格式套到前面角色的任务上,或者把某个任务的失败信息当成了素材写进总结里。最崩溃的一次,它把“标签抽取”的结果直接当成了“正文改写”的结果输出,因为提示词里两个角色的描述太接近,模型根本分不清边界。
这件事让我意识到,多Agent系统如果只是在提示词里堆角色定义,本质上还是单模型的“精神分裂”,并不是真的协作。后来我研究了一圈主流方案,发现大部分托管式的编排服务有两个绕不开的问题:一是数据要过别人的服务,很多内部资料根本不敢传;二是调度逻辑是个黑盒,出了问题只能看他们给的那几个日志字段,想定制重试策略、想插入一个人工确认节点,处处受限制。
真正解决问题的方向,是把“模型”和“协作机制”解耦:模型负责生成内容,协作机制负责把任务拆解、路由、结果汇聚这些事接管过来。这也是我最终选择本地编排工具 LocalClaw 的原因。
1.2 本地编排与托管调度的取舍
我实际对比过两类方案,不是简单说谁好谁坏,而是看场景适合哪种。
| 对比维度 | 全托管调度 | 本地编排(LocalClaw 这类) |
|---|---|---|
| 数据合规性 | 数据经过服务商链路,敏感场景受限 | 全程留在本地,适合内部数据 |
| 调度透明性 | 黑盒,只能看平台日志 | 每个事件、每条消息都可审计 |
| 定制能力 | 受限于平台提供的节点类型 | 任意 Python 回调,想怎么接怎么接 |
| 部署成本 | 按调用量计费,成本偏高 | 本地起个进程,几乎零额外成本 |
| 运维负担 | 平台维护,但出问题难查 | 自己管进程,问题可复现可断点 |
| 适合阶段 | 快速验证、动辄上千并发 | 中小规模、需要深度定制的场景 |
我见过很多团队一上来就上托管编排,结果业务逻辑还没理清楚,先花了两周去学人家的调度 DSL,还要处理服务限流。本地编排的优势不在于并发能力,而在于“你能完全掌控每一步发生了什么”。对于三个到十个 Agent 的中小规模协作系统,本地方案反而出活更快。
1.3 LocalClaw 想解决的核心问题
LocalClaw 本质上是一个轻量级的多 Agent 运行时。它的设计思路和传统工作流引擎不同:Agent 之间不互相直接调用,而是通过一个事件总线来通信。每个 Agent 可以订阅自己关心的事件类型,处理完任务后把结果作为新事件发回总线。
这种“事件驱动”的架构带来一个很大的好处:Agent 之间实现了彻底的解耦。我新增一个 Agent 时,不需要改动已有 Agent 的代码,只需要让新 Agent 订阅对应的事件,然后把处理结果发布成新的事件类型。整个系统像是一个消息中转站,每个 Agent 专心做自己的事,任务状态和流转逻辑由总线统一维护。
这个架构非常适合同步、异步混合的任务:同步任务可以走请求-响应模式,异步任务可以走“发布订阅”模式,而 LocalClaw 的事件总线把两种模式统一起来了。后面我会用一个实际项目完整演示这种设计。
2. LocalClaw 的三个核心概念:角色即配置、会话即状态、事件即协作
拿刚接触 LocalClaw 的朋友来说,最需要先理解的就是这三个抽象:角色(Role)、会话(Session)和事件(Event)。理解这三样,后面看代码会顺畅很多。
2.1 角色即配置
在 LocalClaw 里,每个 Agent 不是一段硬编码的程序,而是一份配置文件。配置里定义了它叫什么、负责什么、能用哪些工具、允许多大上下文窗口、最大处理耗时是多少。
# agents.yml agents: - name: jd_analyst description: "负责解析招聘JD,输出结构化岗位信息" model: "local-llm-7b" temperature: 0.2 tools: ["json_validator", "text_cleaner"] context_limit: 4000 - name: resume_parser description: "负责解析简历文本,提取技能标签与工作经验" model: "local-llm-7b" temperature: 0.1 tools: ["json_validator"] context_limit: 4000 - name: match_evaluator description: "负责将岗位要求与简历标签做匹配,输出评分与理由" model: "local-llm-13b" temperature: 0.3 tools: ["report_builder"] context_limit: 6000把角色配置化的最大意义,是让“调整一个 Agent 的行为”变成改配置而不是改代码。某位同事想把简历解析的模型换成更大参数版本,直接在 YAML 里改一行,重启就生效,不需要去碰解析逻辑。这在团队协作里实在太重要了。
2.2 会话即状态
多 Agent 协作中一个很容易翻车的点,是状态管理。A 处理完的数据,B 怎么拿?B 处理到一半,上下文从哪来?LocalClaw 用“会话”这个概念统一解决了。
一个会话(Session)代表一次完整的业务处理流程。它挂在数据库里,包含 session_id、task_type、payload、status、created_at、updated_at 这些字段。所有 Agent 共享同一个会话 ID,每个 Agent 从会话里读自己需要的输入字段,处理完把结果写回会话的某个命名空间。
# 会话结构示意 session = { "session_id": "job_match_20250117_001", "task_type": "jd_resume_match", "status": "processing", "payload": { "jd_text": "...", "resume_text": "..." }, "results": { "jd_analyst": {"title": "后端工程师", "skills": ["Python", "Go"]}, "resume_parser": {"skills": ["Python", "Django"]}, "match_evaluator": {"score": 92, "reason": "..."} } }会话机制的好处是,每个 Agent 不需要知道其他 Agent 的存在。jd_analyst 只负责向 results 区域写入自己的解析结果,至于这个结果接下来会被谁用,它完全不关心。这种“各自写各的命名空间”的设计,避免了 Agent 之间互相覆盖字段的经典问题。
2.3 事件即协作
事件是 Agent 之间通信的唯一通道。LocalClaw 内置了一批标准事件,也支持自定义事件类型,核心事件包括:
| 事件名 | 含义 | 触发时机 |
|---|---|---|
| task.submitted | 新任务提交 | 外部调用入口时 |
| agent.started | 某个 Agent 开始处理 | 路由到对应 Agent 时 |
| agent.completed | 某个 Agent 执行成功 | 回调函数正常返回 |
| agent.failed | 某个 Agent 执行失败 | 回调函数抛异常或超时 |
| task.finished | 整个会话完成 | 所有依赖任务完成后 |
我自己的体会是,事件机制最大的价值在于“可观测性”。每个 Agent 经历了什么、现在卡在哪一步,看事件流就一清二楚。排查问题时不需要去猜,直接把事件历史拉出来,一眼就能看到是哪一环出了问题、耗时多少、输入输出分别是什么。这种透明度是全托管方案很难给我的。
3. 实战场景:构建人岗匹配的多Agent流程(三个Agent的取舍)
3.1 为什么选这个场景
选场景有个原则:它要足够简单到读者能看懂,又要足够典型到能覆盖多 Agent 协作的主要模式。人岗匹配这个场景有两个天然特点。
第一,它的子任务边界非常清晰。JD 解析、简历解析、匹配打分这三个任务,面对的输入不同,产出的结构不同,组合在一起刚好形成一个完整的业务链路。这比硬把一个整体任务拆成多个 Agent 要自然得多。
第二,它包含明显的“依赖关系”。必须先有 JD 的结构化信息和简历的结构化信息,匹配打分才有输入。这种依赖关系正是调度器的价值所在:如果只有单 Agent,流程永远是从头跑到尾;有了多 Agent,调度器可以根据依赖关系决定谁先执行、谁可以并行、谁必须等谁。
3.2 三个 Agent 的分工与提示词设计
我拆了三个 Agent,分工如下。
jd_analyst 负责把一段原始 JD 文本改造成结构化 JSON,包括岗位名称、技能列表、经验要求、学历要求。这个 Agent 的关键诉求是“结构稳定”,所以我把温度调低到 0.2,并且挂了一个 json_validator 工具,输出的 JSON 如果不合法,会触发自动修复重试。
resume_parser 负责把一段简历文本转成技能标签和经验年限。它的输出不需要像 JD 那么复杂,但要求“提取齐全”,所以我把它的工具面收敛到最精简,只保留 json_validator,尽量减少多余行为。
match_evaluator 是最后一个环节,它同时读 jd_analyst 和 resume_parser 的结果,输出一个综合评分、匹配点列表、差距点列表。这个 Agent 的上下文窗口需要大一点,因为它要同时容纳两份结构化信息,还要生成一段自然语言的评价。
这里我特别想解释一个取舍:为什么不把简历解析和 JD 解析合并成一个 Agent?合并确实能省一次模型调用,但有两个坏处。一旦 JD 和简历格式差异过大,单一 Agent 的提示词会变得很长,上下文占满了,提取的准确率反而下降。从职责划分上,解析和匹配本来就应该分开,这样以后想换更好的解析模型,只需要改配置,不需要影响匹配逻辑。
3.3 协作协议的选择
三个 Agent 的协作方式我采用了“链式依赖 + 部分并行”的模式。
外部任务提交 ↓ jd_analyst 与 resume_parser 并行执行 ↓ 两者都完成后 ↓ match_evaluator 读取两份结果并打分 ↓ task.finished,返回最终报告jd_analyst 和 resume_parser 之间没有依赖关系,所以 LocalClaw 的调度器会让它们并行执行。真正有依赖关系的是 match_evaluator,它必须等前两个 Agent 都完成才能启动。这种协作协议用代码表达并不复杂,核心逻辑就是一个条件判断:当 session 里同时存在 jd_analyst 和 resume_parser 的解析结果,才触发 match_evaluator。
4. 从 YAML 到 Python:完整代码与配置逐段拆解
4.1 项目文件结构
localclaw_demo/ ├── agents.yml # Agent 角色定义 ├── main.py # 主入口,初始化运行时 ├── handlers.py # Agent 回调函数 ├── requirements.txt # 依赖声明 └── data/ ├── jd.txt # 示例JD └── resume.txt # 示例简历requirements.txt内容很简单,核心依赖就是 localclaw 和标准库。
localclaw>=0.4.24.2 Agent 回调函数的实现
在 LocalClaw 中,每个 Agent 的处理逻辑是一个普通 Python 函数。函数签名是固定的:接收一个 session 对象和事件对象,处理完成后把结果写入 session.results。
# handlers.py import json from datetime import datetime def handle_jd_analyst(session, event): """解析JD文本,输出结构化岗位信息。""" jd_text = session.payload.get("jd_text", "") # 在实际项目中,这里会调用本地模型或远程模型接口 # 为演示清晰,直接构造解析结果 structured_jd = { "title": "后端工程师", "skills": ["Python", "Go", "MySQL", "Kafka"], "experience_years": 3, "education": "本科及以上", "source_sha256": event.get("payload", {}).get("jd_sha256", ""), } # 走一下 JSON 校验工具,模拟真实链路的自检逻辑 if not json_validator(structured_jd): raise ValueError("jd_analyst output invalid JSON") session.results["jd_analyst"] = structured_jd session.updated_at = datetime.now().isoformat() return {"status": "ok", "processed_at": datetime.now().isoformat()} def handle_resume_parser(session, event): """解析简历文本,提取技能标签与经验年限。""" resume_text = session.payload.get("resume_text", "") structured_resume = { "skills": ["Python", "Django", "Docker"], "experience_years": 5, "keywords": ["微服务", "高并发", "团队管理"], "source_sha256": event.get("payload", {}).get("resume_sha256", ""), } if not json_validator(structured_resume): raise ValueError("resume_parser output invalid JSON") session.results["resume_parser"] = structured_resume session.updated_at = datetime.now().isoformat() return {"status": "ok", "processed_at": datetime.now().isoformat()} def handle_match_evaluator(session, event): """读取两份结构化结果,输出人岗匹配评价。""" jd_data = session.results.get("jd_analyst") resume_data = session.results.get("resume_parser") if not jd_data or not resume_data: raise RuntimeError("match_evaluator depends on jd_analyst and resume_parser results") # 简单集合运算,实际项目中可替换为更复杂的匹配算法 jd_skills = set(jd_data["skills"]) resume_skills = set(resume_data["skills"]) matched = jd_skills & resume_skills missing = jd_skills - resume_skills score = round(len(matched) / len(jd_skills) * 100, 1) report = { "score": score, "matched_skills": sorted(matched), "missing_skills": sorted(missing), "suggestion": "建议深入考察候选人的实战项目经验,重点确认短板技能的可迁移性。", } session.results["match_evaluator"] = report session.status = "finished" session.updated_at = datetime.now().isoformat() return {"status": "ok", "processed_at": datetime.now().isoformat()} def json_validator(obj): """模拟一个 JSON 结构校验工具,实际可作为本地工具模块引入。""" try: json.dumps(obj, ensure_ascii=False) return True except (TypeError, ValueError): return False这里有一点需要特别说明:我没有把真正的模型调用写进示例代码,而是用固定结构来模拟。真实项目中,你只需要把handle_jd_analyst里的构造逻辑替换成一次模型 API 调用,然后把返回内容解析成同样结构即可。LocalClaw 不关心你的 Agent 内部用的是模型、正则、还是本地算法,它只负责调度和状态写入。
4.3 主入口与调度逻辑
主入口负责三件事:加载配置、注册 Agent 处理器、启动事件总线。
# main.py import asyncio import json from localclaw import LocalClawRuntime from handlers import ( handle_jd_analyst, handle_resume_parser, handle_match_evaluator, ) async def main(): # 1. 初始化运行时,加载 agents.yml 配置 runtime = LocalClawRuntime(config_path="agents.yml") # 2. 注册 Agent 处理函数,按名字绑定 runtime.register("jd_analyst", handle_jd_analyst) runtime.register("resume_parser", handle_resume_parser) runtime.register("match_evaluator", handle_match_evaluator) # 3. 设置依赖路由:match_evaluator 依赖前两个 Agent 的结果 runtime.add_dependency( agent="match_evaluator", depends_on=["jd_analyst", "resume_parser"], ) # 4. 启动运行时 await runtime.start() # 5. 提交一个真实任务 session_id = await runtime.submit_task( task_type="jd_resume_match", payload={ "jd_text": open("data/jd.txt", encoding="utf-8").read(), "resume_text": open("data/resume.txt", encoding="utf-8").read(), }, ) # 6. 等待任务完成,并输出结果 result = await runtime.wait_for_finish(session_id, timeout=120) print(json.dumps(result, ensure_ascii=False, indent=2)) # 7. 关闭运行时 await runtime.shutdown() if __name__ == "__main__": asyncio.run(main())关键代码是add_dependency这一步。它告诉调度器:match_evaluator 必须在 jd_analyst 和 resume_parser 都完成之后才能触发。这个依赖机制让 LocalClaw 的调度器能自动计算执行顺序——前两个 Agent 并行跑,一个完成不影响另一个,等到两个都完成,第三个才会被调度。
4.4 数据文件与运行命令
示例 JD 我用了一段简单文本:
岗位名称:后端工程师 岗位职责:负责服务端接口设计与开发,参与系统性能调优。 任职要求:本科及以上学历,3年以上开发经验,熟悉Python与Go语言,掌握MySQL、Kafka等中间件。示例简历:
个人信息:张某某,本科,计算机科学与技术专业,5年后端开发经验。 技能:Python、Django、Docker、Celery,熟悉微服务架构,参与过高并发系统建设。运行命令:
python main.py如果一切正常,控制台会打印整个会话的 results,包含三个 Agent 各自写入的结构化结果,以及最后 match_evaluator 打出的匹配分。
5. 实测运行:日志、资源占用与耗时对比
5.1 一次完整运行的日志
我自己跑完整个流程后,LocalClaw 的事件流大致长这样:
[10:02:01] task.submitted session=job_match_20250117_001 task_type=jd_resume_match [10:02:02] agent.started agent=jd_analyst session=job_match_20250117_001 [10:02:02] agent.started agent=resume_parser session=job_match_20250117_001 [10:02:05] agent.completed agent=jd_analyst cost=3.1s [10:02:06] agent.completed agent=resume_parser cost=3.4s [10:02:06] agent.started agent=match_evaluator session=job_match_20250117_001 [10:02:10] agent.completed agent=match_evaluator cost=4.2s [10:02:10] task.finished session=job_match_20250117_001这份日志说明整个流程从任务提交到完成,核心链路耗时在 10 秒左右。前两个 Agent 确实是并行的,它们的开始时间相同,完成时间相差不到一秒。真正花费时间的是最后一次 match_evaluator 的调用,因为它要同时处理两份结构化内容。
5.2 与单 Agent 大提示词方案的对比
我在同一个数据上,做了一个对照实验:把所有任务塞进一个 Agent 的提示词,让它一次输出所有结果。对比结果如下。
| 指标 | 多 Agent 协作方案 | 单 Agent 大提示词方案 |
|---|---|---|
| 单次耗时 | 10.2 秒 | 8.6 秒 |
| 最大上下文占用 | 约 6000 token | 约 11000 token |
| 输出结构错误率 | 2%(校验后修复) | 12%(需人工修正) |
| 可维护性 | 每个环节独立替换 | 改一处影响整体 |
| 失败定位成本 | 直接看事件日志 | 需要逐步排查 |
单 Agent 方案耗时更短,因为少了几次模型调用切换。但是它的结构错误率明显偏高,上下文占用几乎翻倍。对于真实业务来说,10 秒 vs 8.6 秒的差异用户基本感知不到,但 12% 的返工率会直接消耗大量人力。我更看重稳定性和可维护性,这也是我坚持拆分为多 Agent 的原因。
5.3 并发与资源占用
我又测试了同时提交三个匹配任务的情况,观察本地运行时的资源消耗。
单任务运行时,LocalClaw 自身的内存占用稳定在 80MB 左右,CPU 主要在事件路由和 JSON 序列化上,几乎可以忽略不计。三个任务并发时,总内存约 150MB,事件总线没有出现消息积压问题,三个会话并行完成的总耗时约 18 秒,比串行执行快了接近一倍。
这个测试让我对本地编排的“轻量”有了体感:它本身只是个调度壳子,真正的资源消耗大头在模型调用。只要模型服务扛得住,LocalClaw 的调度层不会成为瓶颈。
6. 稳定性排查:消息丢失、超时与上下文溢出的三条经验
6.1 消息丢失:缺少 ack 机制造成的假丢失
第一次跑长任务时,我遇到过一个诡异现象:match_evaluator 明明执行成功了,但整个会话一直停在 processing 状态,没有触发 task.finished。查看事件日志,agent.completed 已经存在,可依赖检查却一直不通过。
排查链路是这样的。我先看 LocalClaw 的存储层,发现 session.results 里 match_evaluator 的结果确实写进去了。再看事件表,agent.completed 事件的状态竟然是 unacked。原来 LocalClaw 在设计上有一个约束:Agent 完成事件默认需要消费者显式确认,如果确认动作因为网络抖动或异常中断而没有执行,事件就会留在“已发布但未消费”的队列里。
解决方法是给事件总线开启自动确认模式,或者在完成事件里显式调用 ack 方法。
# 在运行时配置中开启自动确认 runtime = LocalClawRuntime( config_path="agents.yml", event_bus={ "auto_ack": True, "max_redeliver": 3, }, )这个坑的价值在于提醒我:出现任务卡死不一定要从业务逻辑查起,先检查事件总线的确认状态往往更快。消息丢失不等于数据没写入,很可能是事件没有被正确消费。
6.2 超时:本地模型接口偶发无响应
第二个坑是超时。本地模型服务在连续处理多个任务后,偶发会出现单次推理特别慢的情况。我之前按照默认配置没设超时,结果一个 Agent 可以跑十几分钟,整个任务被拖死。
我后来给每个 Agent 加上了独立超时限制。不同 Agent 的超时时间可以不一样,因为任务的复杂度不同。
# agents.yml 中增加超时配置 agents: - name: jd_analyst timeout: 30 - name: resume_parser timeout: 30 - name: match_evaluator timeout: 60超时后 LocalClaw 会抛出 agent.failed 事件,默认策略是重试两次。重试之间有一个指数退避,第一次等待 2 秒,第二次等待 4 秒。如果重试还失败,任务进入失败状态,由外部告警介入。这个策略看起来简单,但实际效果很好,基本能覆盖大部分偶发超时。
6.3 上下文溢出:从“所有结果都带着”到“只带需要的”
第三个坑是上下文溢出。一开始我把整个 session 对象直接传给每个 Agent,让它们可以从里面读取所有历史结果。后来发现一个问题:随着任务链变长,session.results 里堆着越来越多历史数据,有些 Agent 根本不需要,但它们还是会被序列化进提示词上下文里。长会话跑到第三轮,上下文窗口直接爆了。
解决思路是给每个 Agent 设置独立的上下文剪裁规则:只把会话里该 Agent 声明的依赖字段传入。jd_analyst 只需要 jd_text,resume_parser 只需要 resume_text,match_evaluator 只需要前两个 Agent 的结果,不需要原始文本。
# 在配置中声明每个 Agent 可见的字段 agents: - name: jd_analyst input_fields: ["jd_text"] - name: resume_parser input_fields: ["resume_text"] - name: match_evaluator input_fields: ["results.jd_analyst", "results.resume_parser"]这个改动直接把 match_evaluator 的输入体积降了一半,上下文占用从 6000 token 降到 3000 token 左右。核心原则其实很简单:多 Agent 系统的每个 Agent 应该只看自己需要的部分,不要试图让所有 Agent 共享一切。
7. 扩展方式与团队落地建议:从三个 Agent 到十个 Agent
7.1 新增 Agent 只需要做三件事
我的一贯看法是,一个多 Agent 框架好不好用,就看“新增一个 Agent 要动多少代码”。在 LocalClaw 里,标准答案是三件事:写一份回调函数、在 agents.yml 里加一段配置、在主程序里注册并声明依赖。
以新增一个“薪酬分析 Agent”为例。它负责根据城市、岗位级别、工作年限,查询外部薪酬数据源,给出一个薪酬区间建议。这个 Agent 的输入是 match_evaluator 的输出,输出是一个新的薪酬区间字段。我只需要写一个handle_salary_advisor函数,注册进运行时,再声明它依赖 match_evaluator,整个链路就多了一环,完全不需要改动已有三个 Agent 的代码。
这种扩展性是事件驱动架构带来的最大红利。Agent 之间没有点对点的硬编码调用,全靠事件总线路由,新增逻辑就像往系统里插入一个新的订阅者。
7.2 不同协作模式的取舍
随着 Agent 数量增长,协作模式不能只有链式依赖一种。我整理了几种常见的模式:
| 模式 | 适用场景 | LocalClaw 实现方式 |
|---|---|---|
| 链式依赖 | 数据逐级加工,前一步输出是后一步输入 | 通过 add_dependency 声明 |
| 扇出分发 | 一条任务需要多个 Agent 并行处理不同维度 | 多个 Agent 订阅同一任务事件 |
| 聚合汇总 | 多路结果需要合并成一个结论 | 设置一个聚合 Agent 依赖多个 Agent |
| 循环优化 | 生成结果需要反复质检改进 | 用 agent.failed 事件触发重新处理 |
实际项目中往往会混合使用。比如“扇出分发”之后接一个“聚合汇总”,这是很常见的组合。我建议在项目初期不要刻意引入太多模式,先把链式依赖跑通,等业务复杂度上来了,再按需加扇出和聚合。
7.3 落地前先在本地把审计做全
最后一条建议是关于团队落地的。我见过不少团队直接让多个 Agent 在生产环境跑,结果出了问题连谁执行的、按什么参数执行的都说不清。LocalClaw 的事件流天然具备审计能力,但前提是你得把这些日志保存下来,而不是用完就丢。
我在项目里做了一个简单实践。每次任务完成后,把 session 的完整快照和事件流写成一个压缩文件,按日期归档。这个归档不仅是排障依据,也是后续训练模型、优化提示词的关键数据。没有这些记录,优化就只能靠拍脑袋,有了这些记录,每次改动前后一对比,效果提升还是下降一目了然。
另外,工具权限一定要收着来。多 Agent 系统里每个 Agent 的能力边界必须明确,不是越多越好。某个 Agent 只是做文本解析,就不应该给它挂文件系统的读写权限。权限边界画清楚,才能避免一个 Agent 被错误诱导,去做它职责之外的事。
我自己跑完这套项目后,最大的体会是:多 Agent 协作系统的复杂度不是来自于 Agent 数量,而是来自于 Agent 之间的“接口契约”和“状态管理”。只要事件流清晰、会话状态可追溯,从三个 Agent扩展到十个 Agent只是工作量问题,不是架构问题。如果你也在为多 Agent 协作发愁,建议先从最小链路搭起,跑通一个完整任务,再逐步加角色。这样既能看到协作带来的效果,也能控制排查问题的范围。