AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

工具发现:分页收集目录,再选择本次允许的工具

我的学习 →

正在读取学习记录…

1. 工具目录包含什么

工具发现是向服务询问可用工具,并取得名称、说明与输入Schema。模型与应用需要这些信息,才能选择正确工具并构造参数。服务工具多时,目录会分页返回。

分页游标cursor是服务给出的下一页位置。客户端收到nextCursor后继续请求,直到没有下一页。像看分卷目录,需要将各卷条目收齐再选择。

第一页read_note带下一页游标,第二页create_tag结束,合并后划分允许与待核对
分页得到完整目录,应用再用批准列表筛选本次可用工具。

发现目录和批准操作是两项工作:知道服务有create_tag,接下来仍要决定是否向当前任务开放。MCP工具规范定义工具列表、分页和目录变化通知。

2. 收集两页固定目录

新建discovery_lab.py:

先在 PowerShell 创建本篇的独立目录并进入;路径可换成你自己的非系统盘位置:

Bash
mkdir -Force "F:\学习\agentdepth-l2-discovery"
cd "F:\学习\agentdepth-l2-discovery"

将本篇的 discovery_lab.py、test_discovery_lab.py 保存到这个目录。保存全部文件后运行 dir *.py,核对上述文件名,再执行下文命令;新开窗口时先回到该目录,并按 Python 环境篇核对 python 的解释器路径。

Python
"""分页收集工具目录,并比较目录变化。"""

from hashlib import sha256
from typing import Dict, List, Tuple
import json


PAGES = {
    None: ([{"name": "read_note", "inputSchema": {"type": "object"}}], "page-2"),
    "page-2": ([{"name": "create_tag", "inputSchema": {"type": "object", "required": ["note_id"]}}], None),
}
APPROVED = {"read_note"}


# 遍历分页游标,检查重复工具名和重复页。
def collect_pages(pages: Dict) -> List[dict]:
    """沿固定目录的游标翻页,拒绝循环游标和重复工具名。"""
    cursor = None
    seen_cursors = set()
    tools: List[dict] = []
    names = set()
    while True:
        if cursor in seen_cursors or cursor not in pages:
            raise ValueError("bad_cursor")
        seen_cursors.add(cursor)
        batch, next_cursor = pages[cursor]
        for tool in batch:
            name = tool["name"]
            if name in names:
                raise ValueError("duplicate_tool")
            names.add(name)
            tools.append(tool)
        if next_cursor is None:
            return tools
        cursor = next_cursor


# 名称排序后再序列化,让目录顺序变化不影响摘要。
def fingerprint(tools: List[dict]) -> str:
    """供应用核对目录变化的本地摘要,协议未定义这个字段。"""
    ordered = sorted(tools, key=lambda item: item["name"])
    payload = json.dumps(ordered, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    return sha256(payload.encode("utf-8")).hexdigest()[:12]


def review(tools: List[dict], approved: set) -> Tuple[List[str], List[str]]:
    discovered = {tool["name"] for tool in tools}
    return sorted(discovered & approved), sorted(discovered - approved)


if __name__ == "__main__":
    catalog = collect_pages(PAGES)
    allowed, pending = review(catalog, APPROVED)
    print("discovered", [tool["name"] for tool in catalog])
    print("allowed", allowed, "pending", pending)
    print("fingerprint", fingerprint(catalog))
Bash
python discovery_lab.py

输出:

TXT
discovered ['read_note', 'create_tag']
allowed ['read_note'] pending ['create_tag']
fingerprint ccdebd050af3

discovered列出两项。allowed只有read_note,pending是create_tag。fingerprint是目录内容计算出的短摘要,用来比较目录是否变化。

collect_pages跟随游标,seen检查重复页,工具名称集合检查重复工具。fingerprint先按名称排序,再对规范序列化内容计算摘要;目录顺序变化时,内容摘要保持稳定。Schema发生变化,摘要也会变化。

把APPROVED增加create_tag,pending变空,目录摘要不变,因为批准策略与工具内容分别保存。再把一个Schema字段改变,摘要会变化。这分别展示批准列表更新与服务目录更新。

4. 目录更新怎样影响应用

变化处理
出现新工具加入待核对列表
已有工具Schema改变重新检查参数和调用代码
工具被移除更新可选工具,处理尚未发送的动作
工具说明改变更新模型可见说明
收到目录变化通知重新拉取并比较目录

工具名在单服务内唯一,多个服务可用“服务名+工具名”组织,避免碰巧同名。缓存要记录服务身份、协议版本和内容;刷新失败时保留明确状态,执行前再次检查当前工具是否可用。

工具说明来自服务,需要考虑服务可信程度。把目录内容与应用策略分别保存,便于查清“服务新增了什么”和“应用批准了什么”。

总结:分页收齐,检查重复,比较变化,按任务选择。继续读 MCP认证与授权。

5. 验证消息与边界

测试文件保存到同一目录:

下面的测试检查多页工具目录能否合并,并比较工具新增、删除和字段规则变化。

测试代码:test_discovery_lab.py
Python
# 核对工具发现的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest

from discovery_lab import APPROVED, PAGES, collect_pages, fingerprint, review


class DiscoveryTests(unittest.TestCase):
    def test_follow_all_pages_and_keep_review_gate(self):
        tools = collect_pages(PAGES)
        self.assertEqual([item["name"] for item in tools], ["read_note", "create_tag"])
        self.assertEqual(review(tools, APPROVED), (["read_note"], ["create_tag"]))

    def test_catalog_order_does_not_change_local_fingerprint(self):
        tools = collect_pages(PAGES)
        self.assertEqual(fingerprint(tools), fingerprint(list(reversed(tools))))

    def test_schema_change_changes_fingerprint(self):
        tools = collect_pages(PAGES)
        changed = [dict(item) for item in tools]
        changed[0]["inputSchema"] = {"type": "object", "required": ["note_id"]}
        self.assertNotEqual(fingerprint(tools), fingerprint(changed))

    def test_cursor_loop_is_rejected(self):
        with self.assertRaisesRegex(ValueError, "bad_cursor"):
            collect_pages({None: ([], "page-2"), "page-2": ([], "page-2")})

    def test_duplicate_name_is_rejected(self):
        with self.assertRaisesRegex(ValueError, "duplicate_tool"):
            collect_pages({None: ([{"name": "same"}], "next"),
                           "next": ([{"name": "same"}], None)})


if __name__ == "__main__":
    unittest.main()
Bash
python -m unittest test_discovery_lab -v

恢复实验中的改动后运行测试。失败时先查看对应参数、编号或权限条件,再核对程序分支。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“工具发现与能力协商”,哪种做法更稳妥?

对应知识点:L2-discovery

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L2-discovery

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L2-discovery

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L2-discovery

图片查看器
100%

文章配图 · 按 Esc 关闭