构建DeepAgents管理平台:权限设置、子代理与人工审批。
为系统设计规划工具构建DeepAgents框架:文件后端、访问控制规则、子代理、人工审批环节、技能模块以及MCP工具。
来源: The DeepAgents Architecture, Part 2: Building the Agent Harness
在文档中,这种框架很容易让人赞叹,但实际使用起来则更难评判。本文介绍了使用DeepAgents架构构建一个功能完备的应用程序时所经历的情况,包括真实的多步骤设计讨论,以及各项发现与实际证据的对比。这些问题都很实际:该框架如何处理文件和权限,授权机制是怎样的,人工审核的过程如何,以及即便框架正常运行,模型的判断仍会在哪些方面出现失误。
该应用程序是为那些在 Azure 上使用 Databricks 运行数据及 AI 工作的组织设计的系统规划辅助工具。这一选择是经过深思熟虑的。鉴于 Databricks 与微软的 Azure AI 技术栈已深度整合,目前尚无明确的“原生工具”定义,而正是这种开放且多步骤的模糊性,正是该架构旨在解决的难题。完整源代码已作为公共仓库发布。
设置步骤
git clone https://github.com/shubhodayahampiholi/system-design-planner.git
cd system-design-planner
uv sync
cp .env.example .env # fill in real API keys
需要 Python 3.11 或更高版本以及 uv 包管理器。示例中使用了 Anthropic 和 OpenAI 两种模型,因此要完整运行所有示例需准备对应的 API 密钥。而 Tavily 密钥以及 Databricks 工作空间的访问权限仅在使用相关功能的章节中需要。
DeepAgents 中的文件与访问控制
智能体的文件存储位置
智能体在能够读写任何内容之前,首先需要一个地方来存储文件。DeepAgents 提供了多种后端选项,包括 StateBackend、FilesystemBackend,以及基于存储系统或沙箱环境的各种变体。该项目出于不同原因选择了两种后端。
若调用 create_deep_agent() 时不传递任何额外参数,该智能体将使用 StateBackend,这类后端会将文件存储在智能体自身的 LangGraph 状态中。由于每执行一步后都会对该状态进行快照保存,因此只要对话线程持续存在,这些文件就会真正保留下来。但需要注意的是,这种持久化是与特定线程绑定的:在一个线程中写入的内容在另一个线程中是不可见的。对于许多应用而言这正是理想的情况,但此处并非如此,因为无论处于何种对话场景,智能体都必须从磁盘读取真实的参考文档。
FilesystemBackend 可解决这一问题。它为智能体指定一个真实的文件夹,无论是读取、写入还是列出操作,都会操作那些会长期存在的实际文件。所有的示例都源自同一个工厂函数:
# src/system_design_planner/agent.py
from deepagents import create_deep_agent
MODEL = "claude-sonnet-5"
def build_planner_agent(*, system_prompt=PLANNER_SYSTEM_PROMPT, **kwargs):
return create_deep_agent(model=MODEL, system_prompt=system_prompt, **kwargs)
每个示例都是通过该函数来构建其智能体的。当需要实际访问文件时,调用时会传入 backend=FilesystemBackend(root_dir=...)。root_dir 设定了智能体工作的范围,即便它尝试使用如 ../../ 这样的路径,也默认无法超出这个限定范围。智能体在该范围内具体能执行哪些操作,即对哪些路径进行何种操作,则是另一回事,而 FilesystemMiddleware 会通过权限规则来约束这些操作。
决定智能体可执行的操作
访问某个文件夹只是一个决策,而精确限制其内部可进行的操作同样重要。DeepAgents 通过按顺序列出的规则来处理这个问题,这些规则规定了针对每条路径,哪些操作是被允许的、被拒绝的,或是需要用户明确授权才能执行的。以下就是该项目中的智能体所遵循的规则:
# src/system_design_planner/permissions.py
from deepagents import FilesystemPermission
DESIGN_SESSION_PERMISSIONS = [
FilesystemPermission(operations=["read", "write"], paths=["/.env"], mode="deny"),
FilesystemPermission(operations=["write"], paths=["/knowledge_base/**"], mode="deny"),
FilesystemPermission(operations=["write"], paths=["/memory/AGENTS.md"], mode="interrupt"),
]
这些规则会按顺序被检查,每条规则都有其特定的作用。
第一条规则可防止智能体读取或写入存放真实 API 密钥的 .env 文件,无论智能体此时正在执行什么操作,这一规则都会生效。
第二条规则将 knowledge_base/ 目录下的参考资料设置为只读状态。DeepAgents 将 delete 操作视为写入操作,因此该规则同样能阻止删除操作而不仅仅是编辑操作,这样智能体推理所依据的基准数据就不会被其自身操作悄悄破坏。
第三条规则的重要性最高。虽然允许对智能体的内存文件进行写入操作,但必须经过人类审批后方可执行。这一机制将在下文中单独介绍。
确认智能体确实阅读了给定的内容
除非智能体能够正确使用这些资源,否则它们毫无用处,而且实际测试效果往往比预期更好。knowledge_base/文件夹中存放着四份简短的参考文档,内容涉及Unity Catalog治理、Mosaic AI、Microsoft Foundry集成以及与网络相关的身份管理。为确认智能体确实阅读了这些文档而非仅凭文件名猜测,我们将其指向该文件夹并要求其总结其中的内容:
backend = FilesystemBackend(root_dir="knowledge_base")
agent = build_planner_agent(backend=backend)
result = agent.invoke({
"messages": [{
"role": "user",
"content": "List every file in your working directory, then give a one-line summary of what each one covers.",
}]
})
最终得到了四个准确且独立的摘要,每个文件一个。身份与网络相关文档中那些低置信度的表述甚至完整保留了下来,没有像其他三个文档那样被调整为高置信度的风格。执行轨迹解释了这一现象的意义:代理首先调用ls命令查看现有内容,然后针对每个文档调用一次read_file函数,总共四次,之后才开始撰写摘要的任意一个字。这些结论都是基于实际读取的数据得出的。
委托给子代理
此处所说的委托含义
有些工作更适合由专家独立完成,而非混入主代理已需处理的其他事务之中。DeepAgents通过子代理实现了这一点:这些独立运行的代理可被主代理调用来处理特定任务。子代理在独立的上下文中运行,无法看到导致此次调用的完整历史记录,完成工作后会将结果反馈给委托方。这种隔离尤其体现在对话上下文中,但不一定适用于存储,如下文关于内存的讨论所示。
这样做有实际好处。主智能体的注意力能够集中在整体任务上,而非记录专家为得出一个答案所经历的每一个中间步骤。另一个容易被忽视的好处是:由于每个子智能体都是独立配置的,不同的子智能体可以使用完全不同的模型,这些模型是根据各自的任务需求来选择的。
不同子智能体使用不同模型
并非每个子智能体的任务都需要最强大、最昂贵的模型。在这个项目中,有一个子智能体的职责仅仅是从参考文档中提取事实,这属于无需深度推理的机械性任务。因此,它并未使用驱动系统其他部分的模型,而是配备了更适合该简单任务的、体积更小、成本更低的模型:
from deepagents import SubAgent
REFERENCE_EXTRACTOR: SubAgent = {
"name": "reference-extractor",
"description": (
"Extracts structured, factual capabilities from the knowledge_base "
"reference files for a specific platform or topic. Delegate here "
"before proposing any subsystem design, so decisions are grounded "
"in verified platform facts rather than assumption. Do not use this "
"subagent for design reasoning or tradeoffs - extraction only."
),
"system_prompt": (
"You are a fact-extraction specialist. Given a topic, find the "
"relevant file(s) in your working directory, read them, and return "
"ONLY a bullet list of the concrete facts relevant to that topic - "
"no narrative, no design opinions, no recommendations. If a fact is "
"explicitly flagged as an open question or unverified in the source "
"file, preserve that flag in your output rather than smoothing it "
"over into a confident-sounding statement."
),
"model": "openai:gpt-5.6-luna",
}
model字段使得这成为可能。每个子智能体都可以为自己指定模型,而无需考虑主智能体使用的是哪种模型。在当前版本中,主规划器运行在Claude上,而其中一个子智能体则运行在OpenAI模型上。
不过,设置该字段仅属于配置层面,无法确保请求会发送到正确的目的地。要验证这一点,需要查看实际运行过程中的情况。通过逐步查看智能体执行过程的追踪工具,可以发现主智能体发起的调用自始至终都显示在Claude名下,而嵌套在提取子智能体中的调用则显示在其配置中指定的OpenAI模型名下。这是在提供方层面做出的路由决策,并且可以被验证,而非仅仅在代码中声明而已。
子智能体默认会保留自己的内存
在DeepAgents中,隔离是一种刻意的设计选择,其影响范围远超大多数人的初步预期。子代理拥有独立的上下文,同时无法自动访问主代理内存中的任何信息,即便是与分配任务直接相关的内容也不例外,必须明确提供访问权限。
这通常是正确的默认设置,因为子代理无需为了完成一项特定任务而翻阅主代理记录的所有内容。但当任务依赖于已做出并记录下来的决策时,就必须有意识地授予访问权限:
def build_governance_specialist(backend) -> SubAgent:
from deepagents.middleware.memory import MemoryMiddleware
return {
"name": "governance-specialist",
"description": (
"Reasons about Unity Catalog governance, lineage, and the "
"Databricks-Microsoft Foundry governance boundary for this "
"architecture."
),
"system_prompt": (
"You are a governance specialist for an Azure + Databricks AI "
"architecture. Before answering, check /memory/AGENTS.md for any "
"recorded scoping decisions - they are binding constraints on "
"your recommendation, not suggestions."
),
"middleware": [MemoryMiddleware(backend=backend, sources=["/memory/AGENTS.md"])],
}
那个middleware组件负责执行授权操作。该专用模块能够建立与主代理所使用内存文件的独立连接,从而在开始推理之前读取之前的决策结果。
人工审批与持久化记忆
为何某些操作需要人工介入
代理执行的并非所有操作都应无人监督地运行。写入临时文件的风险较低,但记录下后续所有对话都将视为既定事实的决策则完全是另一回事,因为错误不仅会导致一个错误的答案,还可能悄悄影响之后的所有答案。DeepAgents通过引入人工干预机制来解决这一问题:选定的操作可以被设置为暂停执行,直到有真人对其进行批准、拒绝或修改。
这里的机制仅负责一项操作,即向智能体的持久存储文件写入数据。智能体可随时提出更新请求,但只有在得到人类同意后才会进行写入。
批准与恢复的运作方式
要使该机制正常运行,必须满足两个条件。首先,上述权限规则为对存储文件的写入设置了mode="interrupt"模式。其次,也是容易被忽视的一点,暂停操作需要一个检查点,也就是一种能够保存智能体当前精确状态的方法,这样它就可以被暂停和恢复,而无需从头开始重新启动。
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command
checkpointer = MemorySaver()
agent = build_planner_agent(
backend=backend,
permissions=DESIGN_SESSION_PERMISSIONS,
checkpointer=checkpointer,
memory=["/memory/AGENTS.md"],
)
一旦代理尝试进行受保护的写入操作,执行就会真正停止,且文件保持不变。在当前的实现中,可以从图状态中读取该待处理的操作:
state = agent.get_state(config)
if state.interrupts:
action = state.interrupts[0].value["action_requests"][0]
print(f"tool: {action['name']}")
print(f"args: {action['args']}")
输出的内容是代理想要调用的确切工具名称以及它想要保存的原始文本,不会进行任何总结。审核员可以批准它,或者拒绝它并附上具体原因,这些信息会反馈给模型:
decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}
result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)
如果从头到尾完整执行,这个方法可行。当有写入操作被提出时,它会暂时停止,其待处理内容可以在任何人做出决定之前被完整读取,而一旦获得批准,该写入操作就会执行——这一点的确认是通过之后检查磁盘上的文件来实现的,而非依赖代理程序对自己所做操作的描述。
无解释的拒绝
在各种测试中,若直接拒绝某个操作却不说明原因,模型就不会询问出了什么问题。它会编造一个看似合理的理由然后继续执行。有一次,内存写入操作被拒绝且没有给出任何解释,而系统针对该情况的默认提示仅说明操作未执行,除非用户要求否则不应重试。尽管如此,模型还是立即尝试了同一写入操作的修改版本,并用一些根本没人提及的理由为重试辩护。这就是模型对拒绝机制反馈的反应,而非该机制直接导致了这种行为。
实际经验很简单:拒绝操作时务必给出真实且具体的原因。虽然尚未单独测试这种方法是否能在所有情况下都避免此类行为,但它确实消除了模型用猜测来填补信息空缺的最明显可能性。
为何需谨慎处理一次性批准多项决策
第二个发现与拟议的记录方式有关。当设计讨论同时产生多个相关结论时,系统往往会建议将它们全部记录在一次性操作中:一段新的内容,以及一个涵盖其中所有内容的批准记录。
这种方式虽然高效,但也有弊端。在某次记录的案例中,一次批准的记录包含了五项内容。其中四项是该人明确确认的决策,第五项则是系统自行提出的内容,使用了“除非另有指示,否则视为已确定”这样的表述,而非需要得到答复的直接问题。它与那四项已确认的内容一同被存储在系统中,使用的语气同样肯定,权威性也看似相同。
该写入操作已获得完整批准。在审批过程中,并未将那个推测性的条目与四个已确认的条目分开处理,因此快速浏览多条目差异的人更有可能注意到自己明确做出的那四个决定,而非那些用相同措辞隐藏在其中的模糊假设。
这确实是一个需要设计上加以解决的局限。此处的人为干预仅作用于被批准的工具调用层面,而非其中的每一项声明。当一个write_file调用包含了多个逻辑决策时,批准该调用即意味着批准了所有这些决策。在实现更细粒度的审批之前,实际的应对方法就是仔细审查每一项拟进行的记忆写入操作,不要在机制看似可靠后就将审批视为形式而已。
协调多名专家
让智能体自行选择处理问题的人选
迄今为止,所有的子代理都是通过指定要使用的专家来进行测试的。但在实际使用中很少如此:有人提出问题后,代理必须自行判断哪些专家是相关的。测试时用了一个同时涉及治理、AI/ML基础设施、部署和网络的问题,但没有指定任何专家。
最初的回应并非直接委托任务,而是在确定设计方案之前,提出了一系列关于网络拓扑、预期规模以及特定数据库应如何处理的澄清性问题。这是一种合理的做法,并非行动失败,因为其中一些问题的答案会改变哪些专家才是相关的,所以先询问总比猜测要好。
并行委托
在后续的问答中,所有四类专家(治理、AI/ML、编排与网络)都一次性从代理那里收到了任务,而非依次接收。跟踪记录清楚地显示:四次任务委托在同一个响应中一同发出。
这不仅关乎效率问题,也与针对该框架提交的缺陷报告中所描述的情况一致——当多个子代理并行运行且其中一个出现异常时,其他代理也会随之被取消,故障不会仅限于出问题的那个代理。在这次运行中,四类专家都成功完成了任务,因此并未出现那种情况。但真正的并行任务委托恰恰会创造出导致该缺陷出现的条件。
综合处理,而非堆叠处理
更有趣的结果并非委托行为的出现,而是主代理随后如何处理那四个专业意见。较弱的实现方式可能会将每个意见单独列在标题下便止步不前。而实际发生的情况更接近于综合整合:最终建议将这四个输入整合为一个连贯的设计,更重要的是,它发现了单独来看任何一个意见都无法察觉的问题。
网络专家建议通过特定的连接模式访问外部数据库。治理专家则解释了Unity Catalog如何自动追踪原生Databricks服务的数据来源。对于那种特定的集成路径,这两种方法并不像看上去那样能够完美契合。要发现这一问题,就需要同时牢记这两种方案的要点,并找出它们之间的差异,而这项综合分析正是做到了这一点。它明确指出了该设计中的缺陷,并建议通过手动审计日志来弥补,既不会让问题悬而未决,也不会将其变成关于外部连接的一般性规则。
两个获批但意见相左的决策
另一项独立测试发现了更令人担忧的问题。在连续两次内存写入操作中,每次都单独进行了审核并获得通过,但关于模型服务的建议却自相矛盾。第一次建议通过 Foundation Model API 来调用主模型;而稍后在同一会话中通过的第二次建议则要求使用预配置的吞吐量,明确不使用 Foundation Model API,这与之前保存的建议完全相反。
这两项建议都通过了审核。由于每次写入都被视为独立的提案,没有与几分钟前保存的内容进行对比,因此没有检测到这种矛盾。这与“捆绑处理”是不同的问题:捆绑处理关注的是某个决策的审批细节,而这里的问题在于同一对话中做出的不同决策之间缺乏一致性检查。
这两项发现共同揭示了一个缺陷:目前的审批流程仅判断拟进行的写入操作本身是否可接受,而不会检查其是否与已有约定一致。除了这一明显的设计局限外,仅靠审批步骤本身并不能确保安全。
为智能体赋予新能力
自定义工具
迄今为止所有操作都使用DeepAgents内置的工具:读取文件、写入文件、任务委托。实际应用通常需要更多功能,而通过框架内部的相同机制,编写自定义工具并交由智能体使用其实非常简单。
该项目需要一种内置功能所不具备的能力:一种用于检测静态参考资料中的内容是否过时的方法。knowledge_base/ 文件仅是某一时刻的快照,在创建时是正确的,但不会自动更新。而借助实时网络搜索服务开发的自定义工具则填补了这一空白:
from langchain_core.tools import tool
from langchain_tavily import TavilySearch
_tavily_instance = None
def _get_tavily() -> TavilySearch:
global _tavily_instance
if _tavily_instance is None:
_tavily_instance = TavilySearch(max_results=3, topic="general")
return _tavily_instance
@tool
def check_current_standards(query: str) -> str:
"""Search the live web to verify whether a platform capability, naming,
or integration detail is still current.
Use this specifically to check something already pulled from
knowledge_base against what's true right now - not for open-ended
research.
"""
result = _get_tavily().invoke({"query": query})
return str(result)
@tool装饰器以及文档字符串中的描述是让该工具能够被使用的关键。智能体会读取这些描述来判断该工具是否适用于当前要执行的任务,因此清晰、具体的描述与其对应的代码同样重要。将工具绑定到智能体上需要一个参数:build_planner_agent(tools=[check_current_standards])。
仅凭答案内容无法证明其是否确实获取了实时信息,因为模型可以在不调用任何功能的情况下生成看似合理且针对特定日期的答案。真正的证据在于调用记录。在要求验证某个产品的名称变更及其当前状态时,调用记录显示确实调用了check_current_standards函数并返回了真实结果,最终答案便是基于该结果生成的,其中还包括根据实际当前日期准确计算出的特定截止日期倒计时。
技能:智能体可重复使用的流程
另一种能力是书面流程:针对某一类情境提供分步指导,说明如何正确处理,而非针对某个事实或某个动作。DeepAgents通过技能来实现这一点,这些独立的文件描述了代理可在需要时自行查阅的流程。这种“仅在需要时”的设计是刻意为之的,通常被称为渐进式披露。代理始终会看到每个技能的简短名称和描述。只有当代理判断某项技能相关时,整个流程才会被纳入其工作上下文。
一个技能对应一个Markdown文件,文件顶部有一小段结构化信息:
---
name: native-tool-scoping-check
description: Checks whether a request to use "native tooling" is genuinely unambiguous, given the deep current integration between Databricks-native and Azure-native (Microsoft Foundry) services.
license: MIT
---
# Native-Tool Scoping Check
## The procedure
1. Check /memory/AGENTS.md for an existing scoping decision covering this
question. If one exists, treat it as binding and stop here.
2. If no decision exists, do not assume either interpretation - surface
the ambiguity explicitly as a scoping question.
3. Once resolved, the decision should be persisted to memory, subject to
human approval.
要让子代理获得某项技能的访问权限,只需一行代码:"skills": ["/skills/project/"]。该路径即被视为已注册的技能来源:技能中间件会扫描该路径,查找结构合理的子目录,且每个子目录中都必须存在SKILL.md文件。若指向某个随机文件夹,则无法使其成为技能,因为中间件要求路径下必须存在完全符合结构的文件。
有两点需要确认,因为它们对应不同的问题。第一点是该技能是否真的被加载使用了,而非闲置状态。实际运行记录显示,代理程序明确请求了该技能的文件,这证明了相关机制确实在起作用,而不仅仅是存在而已。第二点是该技能的内容是否以有实际意义的方式影响了行为。测试时需要一个可能同时涉及两项技术技能的场景——一项是检索系统设计,另一项是数据管道的可靠性——并观察结果是否同时运用了这两项技能。事实确实如此:所提出的设计采用了其中一项技能中描述的分块方法,同时将重新上传的文档视为可安全替换的内容而非重复项,这遵循了另一项技能中的相关指示。
通过MCP连接外部系统
目前的工具与技能都存在于项目的代码中。而模型上下文协议(MCP)的运作方式则不同。它是一种标准,能够让智能体在远程服务器上查找并调用工具,无需为每个工具单独编写集成代码。
Databricks 运行着一台托管的 MCP 服务器,该服务器会在相应的权限范围内将 Unity Catalog 函数作为 MCP 工具进行发布。智能体可以查找并调用这些受管理的函数,而无需人为为其编写 Python 封装代码。连接方式如下:
import asyncio
from databricks.sdk import WorkspaceClient
from databricks_langchain import DatabricksMCPServer, DatabricksMultiServerMCPClient
async def _fetch_uc_function_tools():
workspace_client = WorkspaceClient()
host = workspace_client.config.host
mcp_client = DatabricksMultiServerMCPClient([
DatabricksMCPServer(
name="uc-functions",
url=f"{host}/api/2.0/mcp/functions/{CATALOG}/{SCHEMA}",
workspace_client=workspace_client,
),
])
return await mcp_client.get_tools()
def get_databricks_uc_function_tools():
return asyncio.run(_fetch_uc_function_tools())
有一个区别很容易被混淆,因此需要准确区分。Unity Catalog函数与MCP工具并非同一事物。函数是受管控的功能,拥有独立的权限,在Databricks中有固定的位置,其存在独立于MCP之外,且出现时间远早于MCP。MCP服务器则是位于其前面的另一层组件。它并非通用的转换器,因为它还参与身份验证和受管控的访问控制,这意味着通过它发起的调用依然会遵循谁可以调用哪个函数的规则。这一点可以直接看出:Databricks中的函数knowledge_platform.default.mcp_verification_check在传递给代理时会被变为knowledge_platform__default__mcp_verification_check,即采用该协议要求的命名格式的相同功能。
为证明该连接是真实的而非模拟的,采用了与自定义工具测试相同的思路:要求获取无法通过猜测得到的结果。Unity Catalog函数被设计为返回事先选定的某个随机值。系统让代理调用该函数并准确报告返回结果,而该函数确实返回了那个精确的值。这证实了确实存在网络往返传输、在Databricks内的执行过程,以及通过协议传回的结果,从而排除了人为伪造的可能性。
交互式界面
超越脚本限制
上述所有内容都是通过从终端启动一次 Python 脚本来运行的,生成输出后便结束。而真正的应用程序通常更像是一种对话:有人输入问题,观察智能体处理过程,并根据其反馈继续提问。该项目的最后一部分则是使用 Streamlit 基于整个项目中使用的同一智能体构建的浏览器界面。
这需要一个从外部看并不显而易见的架构决策。让该项目发挥作用的那些工具,尤其是上面的 Databricks MCP 连接,都需要异步执行能力,也就是说代码可以等待网络响应,同时其他任务仍能继续运行。每当有人与页面交互时,Streamlit 会从头重新运行整个脚本,而当这与耗时的异步任务结合时,就会产生很大的阻碍,尽管 Streamlit 不断添加异步支持功能。这类问题在其他地方已有大量相关记载,因此最好在规划阶段就考虑到这些问题,而非在开发过程中才发现。
对于需要同时服务大量用户的应用,更稳妥的做法是分离不同功能模块:由专门的后端服务来运行智能体,而界面仅作为显示层。对于仅供单个用户使用的本地参考应用,这种分离会增加复杂性却无法带来相应的好处。因此,异步处理会在界面内部完成,通过 Python 内置的 asyncio.run() 函数一次处理一个完整的对话轮次。这是根据该应用的规模所做的刻意权衡,并不意味着这种做法在所有情况下都是最佳选择。
展示智能体的工作状态
界面相比脚本具有优势:它能够实时展示工作进展,而不仅仅是最终结果。该界面会显示实时轨迹,即主代理流程中每次工具调用的列表,并按操作类型进行标注。可视范围仅限于下文所述的边界之内,不会延伸到子代理的独立运行过程中。
这样做意味着需要逐一检查每个工具调用,并判断其属于哪一类:
def classify_tool_call(tool_call, mcp_tool_names):
name = tool_call["name"]
args = tool_call.get("args", {})
if name == "task":
return "subagent", args.get("subagent_type", "?")
if name in mcp_tool_names:
return "mcp", name
if name == "read_file":
path = str(args.get("file_path", ""))
if "/skills/project/" in path and path.endswith("SKILL.md"):
skill_name = path.split("/")[-2]
return "skill", skill_name
if name in ("read_file", "write_file", "edit_file", "ls", "glob", "grep", "delete"):
return "filesystem", name
return "tool", name
在代理运行期间,每个新的工具调用都会被分类并添加到列表中,因此委托给专家的操作与普通文件读取的操作有所不同,而这两者又都与外部MCP调用有区别。在实际使用中,随着对话的进行,这些操作会被区分开来——包括对自定义网络搜索工具的调用、对Databricks MCP工具的调用,每种操作都会有各自的标签。
通过界面批准或拒绝
这里的“人工干预机制”运作方式相同,但人们与之交互的方式有所变化。无需在终端提示符下输入响应,待处理的操作会以带有实际控制按钮的形式显示在页面上:
if st.session_state.pending_interrupt:
action = st.session_state.pending_interrupt
st.warning(f"**Approval needed**\n\n**Tool:** `{action['name']}`\n\n**Args:** `{action['args']}`")
col1, col2 = st.columns(2)
with col1:
if st.button("Approve"):
run_turn(Command(resume={"decisions": [{"type": "approve"}]}))
with col2:
reason = st.text_input("Reason for rejecting")
if st.button("Reject"):
decision = {"type": "reject", "message": reason or "Rejected by the human reviewer."}
run_turn(Command(resume={"decisions": [decision]}))
一次真实的测试中出现了实际的拒绝情况。系统要求该智能体设计系统的另一部分,并建议将其结论保存到内存中。但通过接口进行的写入操作被拒绝了,且给出了具体原因。即便如此,该智能体在结束时的消息仍明确表示这些结论已保存到内存中。
那个说法是错误的。直接检查内存文件,并单独查找项目中可能发生变化的其他文件,确认没有任何内容被写入。底层机制完全遵循了拒绝操作,接口和中断系统也发挥了应有的作用。出问题的是该代理对所发生情况的描述:明明被拒绝了,它却声称操作成功。这与之前终端版本中的行为一致,而现在通过完全不同的执行路径再次得到了验证,这说明这是模型处理被拒绝操作时的固有特性,而非偶发现象。
接口无法显示的内容
早期版本中有一个功能在界面内尚未得到完全验证,那就是技能使用情况。实时追踪数据来自主智能体的活动流,而专家的内部操作步骤,包括技能文件的加载,都在该专家的独立运行环境中进行,其层级低于主流显示的内容。默认情况下,这些内部活动会被合并到主流中,不会单独显示。
LangGraph可以展示这些内容:在调用流式接口时传入subgraphs=True参数,即可获取子智能体运行过程中的活动数据。不过这个选项在当前版本中被省略了,因为它会改变每个流式更新的数据结构,同时还需要重新设计追踪数据的呈现方式。专家使用技能的情况仍可通过之前提到的执行记录来完全确认。该界面只是不显示实时数据,明确说明这一点总比让人产生误解要好。
构建过程所验证的内容
框架依然有效
最初的论点认为,复杂的长流程智能体任务最好被视作状态管理问题。智能体失败并非因为模型无法推理,而是因为它记不清自己知道什么、已经做出了什么决定,或是当前被允许做什么。上述所有内容都以不同的方式验证了这一观点。
在那个问题上,测试结果一直都很理想。能够访问共享内存文件的子代理正是由于这种访问权限而能够正确运行。治理专家也会尊重那些在对话中从未听过的决策,仅仅因为可以查询到这些决策。即便在压力之下,权限规则依然得以维持:在真实界面中反复被拒绝,真人多次点击拒绝按钮,但没有任何一次被拒绝的操作会悄无声息地通过。跨提供商委托——即系统的一部分采用某种模型,另一部分采用不同模型——只是一种配置细节,而非独立的工程项目。
这些机制在性质上各不相同。文件系统权限负责授权,跨提供商路由配置负责设定模型,而子代理内存访问则用于管理状态和上下文。每一项都是为解决状态管理问题而设计的组件,也都按其配置完成了相应功能。这一表述比“整个系统按预期运行”更为精确,接下来的内容将探讨系统在哪些方面未能达到预期。
框架无法解决的难题
有趣的是,有些问题出现在框架运作完美却依然出错的情况下。
系统在没有给出理由的情况下直接拒绝,模型并未询问问题所在。它编造了一个可信的解释并据此行事,两条截然不同的执行路径也印证了这一模式。系统收到了五项需要存储的相关事实,其中四项有人证实,一项仅为推断,但它却以相同的信心和用词记录了全部五项,因此那项推断的结果与旁边已证实的结果强度并无差异。在同一会话中,相隔几分钟进行的两次内存写入分别获得了独立批准,却给出了完全相反的建议,而整个过程中并未出现任何异常提示。
严格来说,这些情况都属于非文件丢失或损坏导致的状态管理故障。由于有合并后的审批记录以及相互矛盾的写入操作,所需信息已被正确存储并可供核查。只是没有人去核查它。拒绝处理的情况更为微妙:模型从未看到拒绝本身,只有关于拒绝的生成消息,因此模型如何处理这些消息就引发了对其自身推理能力的质疑,而与状态缺失无关。在这三种情况中,出问题的是判断能力:直接相信无法解释的拒绝结果、将推测内容与已确认内容混为一谈、未能察觉新答案与旧答案之间的矛盾。这是推理问题而非架构问题,单纯增加结构并不能直接解决它。
为何重要
对于DeepAgents这类工具所针对的问题类型,状态管理是最为关键的要素,而推理质量则是更为复杂、难以仅靠框架本身实现的难题。后端系统、权限控制、隔离式授权以及受限内存机制无法替代良好的判断力。它们能让系统的操作、状态和结果在成为永久性结果之前保持可观察、可追溯且可逆,从而便于事后评估判断力,但并不能保证该判断一定正确。即便被拒绝的写入操作仍被标记为成功,那其实仍是真正的失败,只不过因为系统设计能够验证此类声明而非盲目信任,所以能立即被发现。
归根结底,这正是工具存在的意义。
相关阅读
- LangGraph多智能体工作流:群体模式与监督模式对比 —— 将LangGraph工作流拆分为带有检查点的子智能体,进而构建兼具群体模式与监督模式的金融助手,分析两种模式各自的缺陷所在。
- Claude Code的生产级工具框架:从Slack到合并代码的六层架构 —— 介绍这个六层架构如何将Slack中的任务转化为合并后的代码:事件接收、受控智能体循环、持久化状态、自我修复功能、可观测性以及操作员控制。