LangChain 的 Managed Deep Agents(MDA)此前有个明显的落地瓶颈:通道只有 Slack,Agent 只能服务 Slack 里的人。9 月 24 日发布的 0.8 版本补上了这块:HTTP channels 让托管 Agent 变成一个 HTTP 端点,任何能发 webhook 的外部服务——工单系统、客户门户、你自己的应用——都能触发一次运行;同版还带来双层记忆(全员共享的 agent 记忆 + 按调用方隔离的 user 记忆)、Slack 文件传输与内置的 Parallel 网络搜索工具。本篇按官方文档把 HTTP channels 从声明到部署走通,并顺手把双层记忆配好。先说清边界:MDA 处于公测阶段,当前仅在 LangSmith Cloud 美国区可用,访问需要 LangSmith Plus 或 Startup 档计划。
managed-deepagents>=0.8.0。先理解:HTTP channel 的两段式契约与信任边界
一个 HTTP channel 就是 channels/ 目录下的一个 Python 文件,文件名即通道名与端点路径——channels/orders.py 就对应 /orders。它的核心是两个回调,对应两段契约:verify 负责鉴权,返回 True 放行进入 parse,返回 False 以 401 拒绝;parse 负责把通过校验的请求转成一条消息(启动一次运行)或忽略该事件。两段各自拿到可独立读取的请求体,verify 甚至拿到原始字节 raw_body——这是刻意设计:签名校验必须对原始字节做,而不是对重新序列化后的 body,否则签名永远对不上。另一个关键概念是 provider:它命名外部服务并给调用方 ID 做命名空间,Agent Auth 据此解析「这次运行可以使用谁的凭据」。两个 Shopify 通道共享同一个 provider,同一个调用方会解析到同一个主体;provider 不同的通道即使调用方 ID 相同也互不相通。
verify 是公网请求进入 Agent 运行前仅存的关卡。官方文档原话很直白:端点没有平台级认证,鉴权发生在适配器内部——一个无条件放行的 verify 等于把你的 Agent 裸露在公网。永远校验签名或共享密钥,密钥用部署级 secret 管理,不要写进仓库;verify 抛异常会以 500 拒绝请求,这也可以当作一种防御手段。
Step 1:声明 channel——文件名就是端点路径
# 项目结构
# my-agent/
# agent.py
# channels/
# orders.py # 本篇的主角
# lib/
# orders.py # verify 与 parse 的实现放这里
# channels/orders.py
from managed_deepagents import channels
from lib.orders import parse, verify
channel = channels.http(
provider="orders", # 外部服务命名,给调用方 ID 做命名空间
verify=verify, # 鉴权回调
parse=parse, # 请求转消息回调
)
声明即注册:provider 名只要是任意非空名称即可,声明这个动作本身就把 provider 登记进系统了。工程上有两条约定值得养成:文件名即通道名,起名前想清楚这个端点对外是什么语义;provider 按外部服务聚合——同一个 Shopify 服务拆成订单、退款两个通道时,两边都声明 provider: "shopify",调用方凭据就自然归一到同一个主体,不用重复授权。一个项目可以声明多个 HTTP channel,名称之间不得重复。
Step 2:verify——对原始字节做签名校验
# lib/orders.py
import hashlib
import hmac
import os
from managed_deepagents import HttpChannelRequest
def verify(context: HttpChannelRequest) -> bool:
secret = os.environ["ORDERS_WEBHOOK_SECRET"]
digest = hmac.new(secret.encode(),
context.raw_body,
hashlib.sha256).hexdigest()
received = context.request.headers.get("x-orders-signature", "")
return hmac.compare_digest(f"sha256={digest}", received)
这段官方参考实现浓缩了三条 webhook 安全的基本功:对原始字节算签名(context.raw_body),因为任何框架层的 body 重解析都可能改变序列化结果;用恒定时间比较(hmac.compare_digest)防时序侧信道;密钥从环境解析而不是硬编码。官方还有一条容易踩的坑提示:通道凭据要从环境变量解析,不要用 connections.get——那个接口需要一次运行上下文,在 verify 阶段还没有运行。verify 可以是 async 函数;返回 False 的请求收到 401,抛异常的请求收到 500,两种都算拒绝。
Step 3:parse——把请求变成消息,thread_id 必须是 UUID
# lib/orders.py(承接上面的导入)
import json
import uuid
# thread_id 必须是 UUID:用 uuid5 把外部会话 ID
# 稳定映射成 UUID,同一会话永远映射到同一线程
ORDERS_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL,
"https://orders.example.com")
def parse(context: HttpChannelRequest):
payload = json.loads(context.raw_body)
if payload.get("action") not in ("recheck", "summarize"):
return {"type": "ignore"} # 不关心的事件直接跳过
return {
"type": "message",
"message": {
# 调用方在外部系统的 ID(字符串),
# Agent Auth 据此解析可用凭据的主体
"user_id": str(payload["user_id"]),
# 会话 UUID:同值续同一段对话
"thread_id": str(uuid.uuid5(
ORDERS_NAMESPACE, str(payload["conversation_id"]))),
# 回复目的地(与 agent 线程分开存),
# 即使通道只启动运行也要带上
"target": {"channel": payload["channel"]},
# 触发本次运行的消息文本
"content": payload["text"],
},
}
parse 的返回只有两种形态:消息形态启动运行,{"type": "ignore"} 跳过事件。四个必填字段的语义要逐个吃透:user_id 必须从已验证的数据推导(别信未经校验的字段),数字 ID 记得 str() 转换;thread_id 是全篇最硬的约束——必须是 UUID,其他值一律 400,所以外部会话 ID 要先用 uuid5 做稳定映射,同一个外部会话每次都映射到同一个 UUID,对话才能续上;target 是你在 provider 侧的回复目的地,与 agent 线程 UUID 是两回事;content 是消息文本或 LangChain 内容块列表。字段与校验语义以官方文档为准,不同版本可能微调。
Step 4:部署拿 webhook URL,外部系统开始触发
# 部署(CLI 会列出每个 HTTP channel 的 webhook URL)
mda deploy
# 部署输出示例(结构示意):
# channels/orders -> https://.../orders
# 把 URL 配到你的外部系统,
# 请求头带上签名(与 verify 对应):
# x-orders-signature: sha256=<hmac>
# 失败兜底:运行失败可发送默认错误回复,
# 或注册可选的 error callback 自定义行为
部署完成后,CLI 输出里会列出每个通道的 webhook URL——把它配置进你的订单系统、工单后台或任何能 POST JSON 的服务,记得同时配好签名头。0.8 的运行时对失败运行内置了兜底:可以发送默认错误回复,也可以挂自定义 error callback,避免「Agent 挂了但调用方毫无感知」。联调阶段建议用 mda chat(0.8.3 新增的终端客户端)先在本地把 Agent 本体跑顺,再接外部 webhook,把变量控制在一个维度上。
公测与区域限制先确认。MDA 当前为 public beta,仅在 LangSmith Cloud 美国区可用;HTTP channels 要求 managed-deepagents>=0.8.0(npm 与 PyPI 同版本发布)。另有社区报道指访问 MDA 需要 LangSmith Plus 或 Startup 计划——动手前核对你账号的套餐与区域资格,具体以官方定价页为准。
Step 5:配双层记忆——agent 层共享,user 层按人隔离
# memory.py
from managed_deepagents import MemoryLayer, define_memory
memory = define_memory(
agent=MemoryLayer(), # 共享层:挂载在 /memories/agent/
user=MemoryLayer(), # 隔离层:挂载在 /memories/user/
) # 按调用方身份为键
# 运行时行为:
# - agent 记忆:全体用户共享(团队规则、流程)
# - user 记忆:个人偏好、历史上下文,
# 其他用户不可见
# - 运行时从不在两层之间复制内容
双层记忆是 0.8 的另一个主角,与 HTTP channels 组合刚好覆盖「自建系统里的多用户 Agent」场景:agent 层放团队级知识(升级流程、话术规范),user 层放个人偏好(某位客服喜欢简洁回复、某位客户的历史诉求)。关键设计是运行时不跨层复制内容——共享知识与个人上下文物理隔离,合规审查时能直接说清「谁的什么数据存在哪里」。还要记住通道感知的路由默认值:Slack 私聊两层都可见,Slack 群聊与 HTTP 请求默认只显 agent 记忆——这是为了防止群聊场景下的隐私泄漏,团队可以在策略里调整默认行为,但改之前想清楚你的调用方身份是否足够可信。
Step 6:生产化补齐——凭据、文件与打断恢复
# 1) Agent 自有连接:免浏览器授权流
mda connections create --grant-type client_credentials
# 2) 沙箱文件 API(工具与中间件内):
# runtime.backend 支持 list/read/write/edit/
# search/upload/download(Python: upload_files/
# download_files);未声明沙箱时 backend 不存在
# 注意:backend 不暴露 Context Hub 记忆与技能
# 3) 应用侧 prompt 的打断恢复:
# 用 INTERRUPT_CORRELATION_KEY + correlation_id
# 精确匹配到对应 interrupt;无关联 ID 时
# 仅恢复单个 pending 或开启新轮次
0.8 还有一批生产向的补充能力值得知道。凭据:OAuth 支持面扩到 23 个服务,agent 自有的 client_credentials 连接可以免浏览器授权流,由 MDA 直接获取 token;连接可以经 bearer/basic 包装后用于沙箱代理头,MDA 在每次命令前解析并跟随变更。文件:授权工具与中间件通过 runtime.backend 读写托管沙箱文件,二进制走 upload/download 接口;Slack 场景的入站文件自动存到 /workspace/attachments/。打断恢复:当应用通过 channel 主动发 prompt 并等待用户回答时,多个 pending interrupt 必须用 correlation_id 精确匹配——这是把 Agent 嵌进自有产品界面时最容易漏配的一环。
预期效果与自检清单
全部做完后,你应该达到:mda deploy 输出了 orders 通道的 webhook URL;用正确签名的请求能触发一次 Agent 运行,错误签名收到 401、篡改过的 body 因签名不匹配被拒;同一外部会话 ID 多次触发后,对话在同一个 thread 里延续;双层记忆各写入一条内容后,换一个 user_id 调用看不到别人的 user 记忆;故意触发一次运行失败,调用方收到了默认错误回复。HTTP channels 的意义不止于多一条通道——它把 MDA 从「Slack 里的助手」变成了「可以嵌进任何产品表面的 Agent 运行时」,配合按调用方隔离的凭据与记忆,内部工具和客户面系统终于能走同一套托管设施。剩下的(企业级 IdP 集成、跨会话持久 user 记忆)官方还没给出路线图时间,等后续版本再评估。