跳到正文
LangChain Blog·· 21 天前精选AI 评分62

LangChain 为 Managed Deep Agents 推出 Connections 凭证管理

Connections: managed credentials and per-caller identity for Managed Deep Agents

AI 导读

LangChain 为 Managed Deep Agents 预发布版推出 Connections,用命名凭证替代硬编码 API key,工具在运行时通过 connections.get() 按 slug 读取。

推荐理由

原文给出按调用者身份解析凭证的两种维度与代码示例,可据此判断智能体权限如何从共享密钥转向个人授权。

正文 · AI 翻译

每个 agent 最终都需要代表某人行事——搜索网页、提交工单、发起 pull request。如今,这通常意味着一个 API key 被硬编码到每个部署中,而每个操作都显示在某个服务账号名下。.env 中的 key 回答了 agent 可以做什么。它无法回答是谁提出的请求。

这正是 Connections 要解决的问题。connection 是 LangSmith 工作区中一个具名凭据,你的工具在运行时通过一次调用、按 slug 读取它。

两个维度

一个 connection 有 owner 和 credential type,二者相互独立。

owner 要么是 agent,要么是 caller。agent 拥有的凭据属于该部署,所有 caller 共享它。用户拥有的凭据则在运行时按人解析。

credential 要么是静态密钥,要么是 OAuth 授权:agent 可以持有 OAuth 授权,用户也可以持有密钥。

所有权在你使用 mda connections create 创建 connection 时即固定,而 connections.get() 只会在已存在的凭据中进行选择。

Agent 拥有的密钥

当你需要一个凭据被所有 caller 共享时,就使用 agent 拥有的密钥。对于不因人而异的能力来说,这是正确的做法:网页搜索、地理编码器、定价数据源。

在这个示例中,让我们配置一个到 Tavily 的 connection,为 agent 添加一个通用的网页搜索工具:

uv run mda connections create tavily-agent --secret-from-env TAVILY_API_KEY

tavily-agent 是 slug。它是你给这个 connection 起的名字,也是你的代码使用的名字,不会有任何东西拿它去对照提供商列表进行校验。该值来自 TAVILY_API_KEY 并进入你的 LangSmith 工作区。它不是构建的一部分,mda deploy 也不会像把 .env 扫入部署密钥那样把它扫进去。

读取它的工具是一个普通的 LangChain 工具,只多了一行利用 connections.get 的代码:

# tools/search_web.py
import httpx
from langchain.tools import tool
from managed_deepagents import connections

@tool(parse_docstring=True)
async def search_web(query: str) -> str:
   """
   Search the web.

   Args:
       query: Search query.
   """
   api_key = await connections.get("tavily-agent", {"type": "agent"})
   async with httpx.AsyncClient(timeout=30.0) as client:
       response = await client.post(
           "<https://api.tavily.com/search>",
           json={"api_key": api_key, "query": query, "max_results": 5},
       )
       response.raise_for_status()
       return response.text

如果你需要轮换密钥,可以更新存储在 tavily-agent 的密钥,之后任何 agent 请求都会自动使用新密钥。

使用你自己的应用实现用户拥有的 OAuth

共享 token 很有用,但让你的 agent 代表用户行事,意味着你可以安全地为 agent 提供更多能力。GitHub 与另外 22 个服务一起出现在 connections 目录中,所以你只需提供 client ID 和 secret,别的什么都不用——无需 authorization URL、无需 token URL、无需查找认证方式。

你可以用 mda connections catalog 快速引用目录中的 connection,但只要你自带元数据,就可以连接到任何提供 OAuth 的提供商。

例如,要配置一个到自定义 Github OAuth 应用的 connection:

uv run mda connections create github-issues \
 --oauth github \
 --client-id "$GITHUB_CLIENT_ID" \
 --secret-from-env GITHUB_CLIENT_SECRET \
 --scope repo

在这个示例中,github-issues 是 slug,它属于你,并由你的代码使用。github 是目录服务,它只决定填充哪些端点。

  • -scope repo 会替换目录默认值,而不是追加到其上。GitHub 的
    默认值是 read:user,它无法创建 issue,因此你传入的内容会成为
    完整列表。

工具通过一个 helper 读取 token。在这个示例中,关键的一行是:

access_token = await connections.get("github-issues", {"type": "user"})

只需一次对 connections.get 的调用,已部署的 agent 就可以自动为新用户发起 OAuth 流程,或为之前已针对 OAuth 提供商认证过的用户获取缓存的 OAuth token。

我们可以利用这个 access token 向 Github 发起任意 API 调用:

# tools/github.py
async def _github(method: str, path: str, **kwargs) -> dict:
   access_token = await connections.get("github-issues", {"type": "user"})
   async with httpx.AsyncClient(timeout=30.0) as client:
       response = await client.request(
           method,
           f"{GITHUB_API}{path}",
           headers={
               "Authorization": f"Bearer {access_token}",
               "Accept": "application/vnd.github+json",
               "X-GitHub-Api-Version": GITHUB_VERSION,
           },
           **kwargs,
       )
       response.raise_for_status()
       return response.json()

注意我们设置了 {"type": "user"}。agent 拥有的 connection 在创建时存储了一个值。而这个 connection 完全没有存储值,只存储了应用注册信息。凭据在运行时按 caller 到达——如果该 caller 从未授权过 GitHub,或者其 token 已过期,
connections.get() 会暂停运行并请求授权,而不是直接失败。

那个词只出现一次,就在_github里。一个search_issues工具和一个create_issue工具都从 helper 继承每个调用者的身份,而第三个 GitHub 工具则完全不需要任何认证代码。

收益体现在两处。在写入任何内容之前,search_issues就已经因调用者而异,因为一个人能看到的私有仓库而另一个人看不到,这会改变结果——相同的查询、相同的部署,却得到不同的答案。而当create_issue运行时,issue 会以提问者的身份在 GitHub 上创建。响应中的user.login是他们的用户名,而不是机器人的。

用户自有的 OAuth,无需注册应用

有些 MCP 服务器会自行注册 OAuth 客户端。当它们这样做时,整个设置就是一个 URL。

uv run mda connections create linear-mcp --mcp <https://mcp.linear.app/mcp>

# tools/mcp.py

from managed_deepagents import connections, define_mcp

mcp = define_mcp(
   servers={
       "linear": {
           "transport": "http",
           "url": "<https://mcp.linear.app/mcp>",
           "connection": connections.get("linear-mcp", {"type": "user"}),
       },
   },
)

没有 client ID,没有 client secret,没有应用注册。因为服务器会公布其 OAuth 元数据,并且会为你注册一个客户端,所以你也不需要 scope,连接建立后上面带有read和write,都是从服务器自身的元数据协商而来的。

把它和 GitHub 流程对比一下:一个需要你自己的应用,另一个什么都不需要,而读取它们的代码行是一样的。工具代码正是这里消失的部分——GitHub 需要一个 helper 和两个函数,而这里只需要一个服务器 URL,工具就从 MCP 服务器那里到来了。

一次暂停,列出所有缺失的授权

向 agent 请求跨越两个服务的操作时,运行会在第一个模型回合之前暂停,用一个中断列出调用者尚未授予的所有连接。授权它们后,运行会从停止的地方继续。

项目里没有回调路由,没有 token 存储,没有刷新逻辑,没有同意屏幕。调用者从不打开 LangSmith。

以第二个调用者的身份做同样的事,你会得到第二个 issue,作者不同,但来自同一个 agent、同一个 slug、同一个工作区条目。把它和 Tavily key 对比一下,后者按设计对所有人都是相同的。你可以用以下命令查看你或其他开发者添加到 LangSmith 的连接:

uv run mda connections list

快速开始

Connections 随 Managed Deep Agents 预发布版一起提供,而 OAuth 目录随二进制文件一起提供,所以你手上的版本决定了--oauth接受什么:

uv tool install managed-deepagents
uv run mda connections catalog

agent 自有的凭据属于某个部署,所以在创建之前先搭建并部署一次。之后,每个连接只需三步——创建它,用connections.get()读取它,重新部署以发布读取它的代码。

本地开发也是同样的方式。agent 自有的连接从.env中的MDA_DEV_<SLUG>解析,转为大写并将连字符替换为下划线。用户自有的连接会把已登录的开发者解析为mda dev下的真实主体,因此授权中断会在本地触发,它存储的授权也是真实的。

除了上述三种流程之外,--authorize会为部署存储一个 OAuth 授权,因此每个调用者都以同一个共享账户行事——这是按凭据划分所有者模型的第四格,当你想要一个专用团队账户而不是按人区分身份时,这就是正确的答案。--allowed-scope限制了后续授权可以请求的范围,而--authorize-url配合--token-url可覆盖目录之外的任何提供商。

如需更多细节和示例,Connections 的文档可在 https://docs.langchain.com/langsmith/python/managed-deep-agents-connections 找到

‍

来源:LangChain Blog · langchain.com