SKILL.md 技能文档
能力目标
按工具名精确声明「哪一步要人点头」,其余步骤照常自主执行;中断发生时读懂待审载荷、把四种决策之一送回去让智能体从断点续跑;再按文件路径加一层规则,对某些目录一律硬拦、不打扰人。
前置
- 已能构造智能体(见 bootstrapping-deepagents-env)。
- 中断状态快照由检查点保存器承载。本地起步用进程内实现,进程一退出状态全丢;生产换成本地文件或数据库实现,只替换这一个对象、其余代码不动。
实操流程
建带审批门的智能体,两个参数一起给:
from deepagents import create_deep_agent from langgraph.checkpoint.memory import InMemorySaver checkpointer = InMemorySaver() agent = create_deep_agent( model=MODEL, interrupt_on={"write_file": True}, # 只拦写文件,其余工具自主 checkpointer=checkpointer, # 供恢复时找回暂停状态 )字典的键是工具名、值是审批配置。
True是简写,等价于把四种决策类型全部打开;字典里没出现的工具完全不受影响。触发一次中断并读懂载荷。中断是正常控制流、不是报错——调用正常返回,只是返回里多了一个中断键:
config = {"configurable": {"thread_id": "step2-hitl"}} result = agent.invoke({"messages": [HumanMessage(task)]}, config=config) interrupt_obj = result["__interrupt__"][0] # 是列表,可能有多个待审 payload = interrupt_obj.value # 真实载荷在 .value 里 action_req = payload["action_requests"][0] print(action_req["name"], action_req["args"])载荷里只有「打算用什么参数调用什么工具」,没有结果字段——工具还没执行。
把决定送回去,用同一个会话标识再次调用。四种决策各自的语义与写法:
from langgraph.types import Command # 原样放行 agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config) # 拒绝并附理由 agent.invoke(Command(resume={"decisions": [{"type": "reject", "message": "敏感数据,禁止写入文件系统"}]}), config=config) # 改参数后放行 agent.invoke(Command(resume={"decisions": [{"type": "edit", "edited_action": { "name": "write_file", "args": {"file_path": "/safe_log.txt", "content": "审核通过的安全内容"} }}]}), config=config) # 人代替工具作答 agent.invoke(Command(resume={"decisions": [{"type": "respond", "message": "该项已由人工完成登记"}]}), config=config)恢复对象来自 LangGraph 的类型模块,不是装具框架自己导出的。
要限制可用决策或自定义给审批人看的话术,就把
True换成审批配置对象:from deepagents import InterruptOnConfig custom_config = InterruptOnConfig( allowed_decisions=["approve", "reject", "respond"], description="⚠️ Agent 想要写入文件,请审核!", ) agent = create_deep_agent(model=MODEL, interrupt_on={"write_file": custom_config}, checkpointer=InMemorySaver())加一层按文件路径的权限规则。三种模式各有适用面:放行是默认;硬拒在工具内部同步返回权限错误、不经过人、不需要检查点保存器;导流则触发中断走同一套审批流程、因此需要检查点保存器:
from deepagents import FilesystemPermission deny_rule = FilesystemPermission(operations=["write"], paths=["/secret/**"], mode="deny") agent = create_deep_agent(model=MODEL, permissions=[deny_rule])换自己的危险工具时只改工具名,整套流程不动:
agent = create_deep_agent(model=MODEL, interrupt_on={"edit_file": True}, checkpointer=InMemorySaver())
校验回路
- 中断成立:返回的键里含中断键,载荷里的工具名与参数正是你要拦的那一步。
- 放行后文件出现在返回的文件状态里;拒绝后该路径不在文件状态里,且消息里多出一条状态为错误、内容是你填的理由的工具消息,智能体没有崩溃、而是继续推理并向用户解释。
- 做有无审批门的并排对照当效果铁证:同一任务、同一模型,不配审批门时目标文件被写入,配了审批门并拒绝时该文件不存在。
- 混合工具任务里,审批列表只含被声明的那个工具;同一轮里并行发出的其他工具直接自动放行、不出现在审批列表里。
- 改参数放行后,新路径写入成功、原危险路径不存在。
常见陷阱
- 漏配检查点保存器:中断的触发不依赖它——不配也照样能拿到中断键。它真正的作用是让恢复那一步找回暂停状态。缺了它,恢复时抛「不能在没有检查点保存器的情况下使用恢复命令」的运行时错误。症状随通道而异:按工具名的审批门缺它是显式报错;按路径的导流模式缺它是单向中断,能中断不能恢复、而且不报错,更隐蔽。
- 恢复时用错会话标识:不报错,而是静默开一个全新的空线程——智能体收到空历史、答非所问,而原来那个真正等待审批的任务永远不会被恢复。这比报错更危险。用回正确的标识时,原中断状态一直挂着、仍能恢复。把会话标识当关键参数严格管理。
- 把人代答当成工具执行了:它注入的是一条状态为成功的合成工具消息,工具根本没跑。实测智能体会据此回复「已成功写入」,而文件终态其实是不存在——它以为成功了。用这一决策时,要么由人真正完成了那件事,要么在消息里如实说明,否则智能体后续推理会建立在一个不存在的成功之上。
- 以为中断像协程那样原地冻结:实际是把整张图的状态序列化存好、抛中断退出执行栈;恢复时从对应节点顶部重新执行、已解决的中断值从缓存填回。推论:节点顶部到中断点之间的副作用会被重放一次。实测一次完整流程里那段逻辑被调用三次——中断前一次、恢复重放一次、最终回复时一次,其中真正的重放只有中间那次。生产含义很直接:审批门之前的副作用必须幂等,写库、扣款、发请求要么挪到恢复之后,要么保证可安全重复执行。
- 混淆拒绝与硬拒:拒绝是审批层的人工决定、发生在运行中、需要检查点保存器;硬拒是路径规则在工具执行前同步拦截、不需要人也不需要检查点保存器。两者都注入状态为错误的工具消息,但触发时机与是否打扰人完全不同。想让人逐次审批用导流或按工具名的审批门,想对某些路径一律硬拦用硬拒。
- 拿模型会自我审查的路径做演示:让它写系统敏感文件时,模型的安全过滤会让它直接拒绝、根本不产生工具调用,中断永远不触发、断言失败。演示用普通的业务路径。
- 改参数放行后智能体又重试原意图:这一决策只改了这一次调用的参数,用户的原始请求还在推理历史里。要么在系统提示里补上新规则,要么改用拒绝并说明理由。
- 不同工具的参数名不同:做改参数决策时要查目标工具的参数结构(例如写文件与编辑文件的内容字段名就不一样),照抄另一个工具的字段名会失败。
适用范围与前置条件
- 已能构造智能体(见 bootstrapping-deepagents-env)。
- 中断状态快照由检查点保存器承载。本地起步用进程内实现,进程一退出状态全丢;生产换成本地文件或数据库实现,只替换这一个对象、其余代码不动。
怎么使用
使用步骤
建带审批门的智能体,两个参数一起给:
from deepagents import create_deep_agent from langgraph.checkpoint.memory import InMemorySaver checkpointer = InMemorySaver() agent = create_deep_agent( model=MODEL, interrupt_on={"write_file": True}, # 只拦写文件,其余工具自主 checkpointer=checkpointer, # 供恢复时找回暂停状态 )字典的键是工具名、值是审批配置。
True是简写,等价于把四种决策类型全部打开;字典里没出现的工具完全不受影响。触发一次中断并读懂载荷。中断是正常控制流、不是报错——调用正常返回,只是返回里多了一个中断键:
config = {"configurable": {"thread_id": "step2-hitl"}} result = agent.invoke({"messages": [HumanMessage(task)]}, config=config) interrupt_obj = result["__interrupt__"][0] # 是列表,可能有多个待审 payload = interrupt_obj.value # 真实载荷在 .value 里 action_req = payload["action_requests"][0] print(action_req["name"], action_req["args"])载荷里只有「打算用什么参数调用什么工具」,没有结果字段——工具还没执行。
把决定送回去,用同一个会话标识再次调用。四种决策各自的语义与写法:
from langgraph.types import Command # 原样放行 agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config) # 拒绝并附理由 agent.invoke(Command(resume={"decisions": [{"type": "reject", "message": "敏感数据,禁止写入文件系统"}]}), config=config) # 改参数后放行 agent.invoke(Command(resume={"decisions": [{"type": "edit", "edited_action": { "name": "write_file", "args": {"file_path": "/safe_log.txt", "content": "审核通过的安全内容"} }}]}), config=config) # 人代替工具作答 agent.invoke(Command(resume={"decisions": [{"type": "respond", "message": "该项已由人工完成登记"}]}), config=config)恢复对象来自 LangGraph 的类型模块,不是装具框架自己导出的。
要限制可用决策或自定义给审批人看的话术,就把
True换成审批配置对象:from deepagents import InterruptOnConfig custom_config = InterruptOnConfig( allowed_decisions=["approve", "reject", "respond"], description="⚠️ Agent 想要写入文件,请审核!", ) agent = create_deep_agent(model=MODEL, interrupt_on={"write_file": custom_config}, checkpointer=InMemorySaver())加一层按文件路径的权限规则。三种模式各有适用面:放行是默认;硬拒在工具内部同步返回权限错误、不经过人、不需要检查点保存器;导流则触发中断走同一套审批流程、因此需要检查点保存器:
from deepagents import FilesystemPermission deny_rule = FilesystemPermission(operations=["write"], paths=["/secret/**"], mode="deny") agent = create_deep_agent(model=MODEL, permissions=[deny_rule])换自己的危险工具时只改工具名,整套流程不动:
agent = create_deep_agent(model=MODEL, interrupt_on={"edit_file": True}, checkpointer=InMemorySaver())