qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

请找出全部未完成事项,前十条搜索结果够吗?

做一个逐页收集任务的只读助手,把“已经列全”与“目前只拿到这些”变成程序能检查、用户能看懂的两种结果。

展开本文目录
  1. 下载完整练习包
  2. 先确认工具承诺能列全这个集合
  3. 这一页没有记录,还要不要翻下一页?
  4. 翻到了末尾,也可能混用了两个时刻
  5. 收集器每收一页,都先做一次验收
  6. 运行同一流程,让八种情况给出不同报告
  7. 总数可以帮助检查,但别让它替代结束条件
  8. 报告要同时说“发现了什么”和“读到了哪里”
  9. 离线应用的完成,不等于所有数据源都验证了
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了

你让助手“列出这个小组全部未完成事项”。工具返回十条,助手整理得很清楚,最后写“共十项”。但十可能只是这一页的大小,也可能只是搜索排在前面的十条。摘要没有编造内容,范围却已经说过头了:找到十项,不等于只有十项。

这个项目把任务从读取一直做到报告。我们提供一个本地分页工具,写收集器继续取页、检查范围与快照,最后交出 complete 或 partial。练习不调用模型,也不访问真实任务库。旧的召回实验关注前几条有没有找到相关材料;本篇关注一个明确集合什么时候才能声称已经读完。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本;制作时实测 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • exhaustive_task_report.py:完整示例
  • README.md:环境、范围与独立练习
  • observed.stdout.txt:制作时的观察记录
  • verification.json:对应源码的验证范围
  • SHA256SUMS.txt:文件内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B exhaustive_task_report.py

解压后在含示例文件的文件夹打开 PowerShell,按命令运行。先预测结果,再与当次输出对照。 离线固定数据与应用规则,不代表真实模型、完整协议或服务联调。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先确认工具承诺能列全这个集合

本题将“全部”限定为:当前调用者获准读取的 team-A,在 snapshot-1 这份固定数据视图里,状态字段为 open 的所有任务。它不包括其他小组、读不到的私人事项或工具从未收录的待办。用户若需要更大的范围,应先选择合适的数据源与权限,不能靠把标题改成“全局报告”来扩大证据。

适合这个目标的工具应提供可遍历的列表契约,并说明过滤条件与结束标志。相似度搜索返回最相关的若干项,通常没有因此承诺所有匹配项都可枚举。把前十条改成前一百条,能改变观察范围,仍不能自动得到完整性保证。收集器不能替一个缺少这种承诺的工具创造保证。

项目中的 FakeAPI 明确约定固定快照可遍历,并只允许 team-A。这个限制是演示用检查,不是真实身份认证。Query 把范围、快照和 open 条件固定下来;每次调用传同一份 Query,让后续页的含义始终与第一页一致。

这一页没有记录,还要不要翻下一页?

三页夹具故意这样排:第一页任务 A,带下一页标记;第二页没有任务,仍带下一页标记;第三页任务 B,标记为空。如果看到空列表就结束,会漏掉 B。请求多少条只是每页的上限,不是服务必须返回的数量,也不是“够数就算完成”的条件。

Google AIP-158 允许尚未到末尾时返回少于请求数量的结果,甚至零条;按这份约定,应依据 next_page_token 判断集合是否结束。本例借用这一类契约,用 next_token 表达下一页。遇到别的 API,要先读它自己的规则,不能把空 token 的约定强加给所有分页协议。

标记在客户端应当是不透明字符串:原样传回,别拆出自以为的页码或时间戳,更不要自行加一。示例中的 opaque-2 方便人看,收集器只比较和保存它,从不分析含义。页标记本身也不授予访问权限;演示工具在每次请求中仍检查范围。

翻到了末尾,也可能混用了两个时刻

假如第一页读完后,任务被新增、删除或重新排序,下一页未必与第一页属于同一份集合。有些记录会重复,有些会被跨过去。因此,“看到结束标记”解决的是这轮遍历是否结束;这份遍历能代表哪一时刻,还需要数据源提供相应的一致性条件。

Elasticsearch 的分页文档提醒刷新可能改变跨页结果顺序,并提供 PIT 来保留搜索时的索引状态。这是一个具体服务提供稳定视图的例子。本项目没有启动 Elasticsearch,只在固定夹具中显式标注 snapshot-1;第二页如果突然回显 snapshot-2,收集器丢弃该页并返回 partial。

本例的 snapshot 是应用约定的逻辑快照标识,要求相同才可合并;它不等同于任何产品的原始分页标记。某些服务延续同一视图时也会更新访问它的句柄,例如 Elasticsearch 文档要求后续使用最新 PIT ID。接入真实工具时,应按其合同识别逻辑视图,不能照抄字符串相等规则。

收集器每收一页,都先做一次验收

collect 记录已经请求的标记、已经接纳的页面数与按任务 ID 整理的结果。收到页面后,先检查范围、过滤条件和快照,再检查行里的范围与未完成状态。整页通过后才加入报告,避免读到一半才发现混入别的小组记录,前半页却已被当成可靠结果留下。

同一 ID 与完全相同的内容再次出现时,只保留一份。若同一 ID 的标题变了,不能仅因它“更晚收到”就覆盖旧内容;固定快照出现这样的冲突值得停下来检查。示例将当前页整体拒绝,保留此前已验收的页,报告 conflicting_duplicate。

每个请求都消耗页预算,不论该页有没有记录。重复的继续标记得到 token_loop;不断生成新标记也最多请求预算允许的次数。工具失败时输出 tool_error。这些都是有限结束,不是成功完成;用户仍能拿到已确认的部分结果和中断原因,而不会陷入没有边界的循环。

运行同一流程,让八种情况给出不同报告

运行前先判断:空中间页、只允许读两页、标记循环、快照改变、工具失败、重复内容冲突、来源声明搜索不完整、总数只是估计,各应显示哪种状态。然后运行完整脚本。Python 3.10 或更新版本即可,所有数据都是代码内的夹具,不需要账号、网络或模型额度。

第一行应读三次并找到 A、B。预算为二时只保留 A 且标为 partial;快照变化时不能接纳另一快照的 B。脚本还检查相同重复可去重、已拿够精确总数仍继续取末页、真实空集合可完成、被拒绝的范围不能报告成完整空集合,以及调用者改夹具不会改变工具已保存的副本。

展开收起代码 · 小实验python · 215 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Offline exhaustive read workflow. Fixed fixtures; no network or real auth."""
from copy import deepcopy
from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Query:
    scope: str = "team-A"
    snapshot: str = "snapshot-1"
    state: str = "open"


@dataclass(frozen=True)
class Task:
    id: str
    title: str
    scope: str = "team-A"
    done: bool = False


@dataclass(frozen=True)
class Page:
    rows: tuple
    next_token: str
    scope: str = "team-A"
    snapshot: str = "snapshot-1"
    state: str = "open"
    search_complete: bool = True
    total: int | None = None
    total_kind: str = "unknown"  # unknown, estimate, exact


@dataclass(frozen=True)
class Report:
    status: str
    reason: str
    query: Query
    tasks: tuple
    requests: int
    accepted_pages: int


class ToolFailure(Exception):
    pass


class FakeAPI:
    """This teaching contract lists all matching rows in a fixed snapshot.

    The server fixture copies its pages and checks one authorized scope.
    This is not an implementation of authentication or snapshot isolation.
    """
    def __init__(self, pages):
        self.pages = deepcopy(pages)
        self.calls = []

    def list_tasks(self, query, token):
        self.calls.append((query, token))
        if query.scope != "team-A":
            raise ToolFailure("forbidden scope")
        page = self.pages.get(token)
        if isinstance(page, Exception):
            raise page
        if page is None:
            raise ToolFailure("unknown continuation token")
        return deepcopy(page)


def collect(api, query, page_budget=10):
    if type(page_budget) is not int or page_budget < 1:
        raise ValueError("page_budget must be a positive integer")
    if query.state != "open" or not query.scope or not query.snapshot:
        raise ValueError("this demo needs an open-task query with an explicit scope and snapshot")
    token, requested, found = "", set(), {}
    requests = accepted_pages = 0
    exact_total = None

    def report(status, reason):
        return Report(status, reason, query, tuple(found.values()), requests, accepted_pages)

    while True:
        if requests >= page_budget:
            return report("partial", "page_budget")
        if token in requested:
            return report("partial", "token_loop")
        requested.add(token)
        requests += 1
        try:
            page = api.list_tasks(query, token)
        except ToolFailure:
            return report("partial", "tool_error")
        if page.scope != query.scope or page.state != query.state:
            return report("partial", "scope_or_filter_changed")
        if page.snapshot != query.snapshot:
            return report("partial", "snapshot_changed")
        if type(page.next_token) is not str or type(page.search_complete) is not bool:
            return report("partial", "invalid_page")
        if page.total_kind not in ("unknown", "estimate", "exact"):
            return report("partial", "invalid_total")
        if page.total is not None and (type(page.total) is not int or page.total < 0):
            return report("partial", "invalid_total")
        if page.total_kind != "unknown" and page.total is None:
            return report("partial", "invalid_total")
        if page.total_kind == "exact":
            if exact_total is not None and exact_total != page.total:
                return report("partial", "exact_total_changed")
            exact_total = page.total

        # Validate a whole page before admitting its rows to the report.
        candidate = dict(found)
        for task in page.rows:
            if (not isinstance(task, Task) or not task.id or task.scope != query.scope
                    or query.state != "open" or task.done is not False):
                return report("partial", "invalid_row")
            if task.id in candidate and candidate[task.id] != task:
                return report("partial", "conflicting_duplicate")
            candidate[task.id] = task
        found = candidate
        accepted_pages += 1
        if not page.search_complete:
            return report("partial", "source_incomplete")
        if page.next_token == "":
            if exact_total is not None and len(found) != exact_total:
                return report("partial", "exact_total_mismatch")
            return report("complete", "end_of_collection")
        # Empty rows and count == total are deliberately not termination tests.
        token = page.next_token


def show(label, result):
    ids = ",".join(task.id for task in result.tasks) or "(none observed)"
    print(f"{label}: {result.status}; {result.reason}; tasks={ids}; requests={result.requests}")


def main():
    task_a = Task("A", "prepare agenda")
    task_b = Task("B", "check venue")
    pages = {
        "": Page((task_a,), "opaque-2", total=2, total_kind="exact"),
        "opaque-2": Page((), "opaque-3", total=2, total_kind="exact"),
        "opaque-3": Page((task_b,), "", total=2, total_kind="exact"),
    }
    query = Query()
    api = FakeAPI(pages)
    complete = collect(api, query)
    assert complete.status == "complete" and complete.tasks == (task_a, task_b)
    assert complete.accepted_pages == 3 and complete.requests == 3
    assert all(sent == query for sent, _ in api.calls)
    show("empty middle page", complete)

    limited = collect(FakeAPI(pages), query, page_budget=2)
    assert limited.status == "partial" and limited.reason == "page_budget"
    assert limited.tasks == (task_a,) and limited.requests == 2
    show("budget reached", limited)

    loop = dict(pages)
    loop["opaque-2"] = replace(loop["opaque-2"], next_token="opaque-2")
    looping = collect(FakeAPI(loop), query)
    assert looping.reason == "token_loop" and looping.requests == 2
    show("repeated token", looping)

    changed = dict(pages)
    changed["opaque-2"] = Page((task_b,), "", snapshot="snapshot-2")
    mixed = collect(FakeAPI(changed), query)
    assert mixed.reason == "snapshot_changed" and mixed.tasks == (task_a,)
    show("changed snapshot", mixed)

    failed = dict(pages)
    failed["opaque-2"] = ToolFailure("fixture timeout")
    failure = collect(FakeAPI(failed), query)
    assert failure.reason == "tool_error" and failure.tasks == (task_a,)
    show("tool failed", failure)

    duplicate = dict(pages)
    duplicate["opaque-3"] = replace(duplicate["opaque-3"], rows=(task_a, task_b))
    deduplicated = collect(FakeAPI(duplicate), query)
    assert deduplicated.status == "complete" and deduplicated.tasks == (task_a, task_b)
    duplicate["opaque-3"] = replace(duplicate["opaque-3"], rows=(replace(task_a, title="changed"), task_b))
    conflict = collect(FakeAPI(duplicate), query)
    assert conflict.reason == "conflicting_duplicate" and conflict.tasks == (task_a,)
    show("conflicting duplicate", conflict)

    partial_search = {"": Page((task_a,), "", search_complete=False)}
    incomplete = collect(FakeAPI(partial_search), query)
    assert incomplete.reason == "source_incomplete" and incomplete.status == "partial"
    show("source warning", incomplete)

    estimated = {"": Page((task_a, task_b), "", total=100, total_kind="estimate")}
    estimate_result = collect(FakeAPI(estimated), query)
    assert estimate_result.status == "complete" and len(estimate_result.tasks) == 2
    show("estimated total", estimate_result)
    mismatch = {"": Page((task_a, task_b), "", total=100, total_kind="exact")}
    assert collect(FakeAPI(mismatch), query).reason == "exact_total_mismatch"

    count_is_not_end = {
        "": Page((task_a,), "still-more", total=1, total_kind="exact"),
        "still-more": Page((), "", total=1, total_kind="exact"),
    }
    assert collect(FakeAPI(count_is_not_end), query).requests == 2
    empty = collect(FakeAPI({"": Page((), "", total=0, total_kind="exact")}), query)
    assert empty.status == "complete" and empty.tasks == ()
    denied = collect(FakeAPI(pages), replace(query, scope="team-B"))
    assert denied.status == "partial" and denied.tasks == ()
    wrong_scope = {"": Page((replace(task_a, scope="team-B"),), "")}
    assert collect(FakeAPI(wrong_scope), query).reason == "invalid_row"
    wrong_filter = {"": Page((task_a,), "", state="closed")}
    assert collect(FakeAPI(wrong_filter), query).reason == "scope_or_filter_changed"
    # Changing the caller's source dictionary cannot change the API snapshot.
    pages[""] = Page((), "")
    assert collect(api, query).tasks == (task_a, task_b)
    print("PASS: bounded exhaustive read, fixed context, partial reports, duplicate checks")


if __name__ == "__main__":
    main()

怎样运行:保存为 exhaustive_task_report.py,用 Python 3.10 或更新版本运行:python exhaustive_task_report.py。只用标准库,不联网、不调用模型,也不安装软件包。 2026-10-13 已用 3.14.0 执行这份示例并通过自带断言;下方为当次输出。离线固定数据与应用规则,不代表真实模型、完整协议或服务联调。

运行后对照结果
empty middle page: complete; end_of_collection; tasks=A,B; requests=3
budget reached: partial; page_budget; tasks=A; requests=2
repeated token: partial; token_loop; tasks=A; requests=2
changed snapshot: partial; snapshot_changed; tasks=A; requests=2
tool failed: partial; tool_error; tasks=A; requests=2
conflicting duplicate: partial; conflicting_duplicate; tasks=A; requests=3
source warning: partial; source_incomplete; tasks=A; requests=1
estimated total: complete; end_of_collection; tasks=A,B; requests=1
PASS: bounded exhaustive read, fixed context, partial reports, duplicate checks

总数可以帮助检查,但别让它替代结束条件

total=100 到底表示精确的一百条,还是约有一百条,必须看字段约定。AIP-158 允许总数是有说明的估计值。本例用 total_kind 明确区分 unknown、estimate、exact;未知不补成零,估计不拿来判断缺了多少,也不因已经收集够估计数就提前停下。

精确总数针对同一范围、同一过滤条件、同一快照的唯一任务数。它在多页之间变化会使报告停止;到末尾后,去重数量与它不相等,也不能宣称 complete。反过来,即使数量相等,仍要按契约走到末页,因为仅有一个计数无法证明 ID 集合正好完整。

总数缺失时,只要可信工具的枚举契约、固定范围与视图、结束标志都成立,仍可在这个限定集合内完成。程序检查的是这些声明之间有没有可见冲突,无法独立证明上游没有漏行或谎报结束。需要更强保证时,应增加独立清单、对账或数据源自身的完整性机制。

报告要同时说“发现了什么”和“读到了哪里”

完整报告可以写:“在 team-A 的 snapshot-1 未完成任务列表内,已遍历至末尾,得到 A、B 两项。”部分报告则写:“目前确认 A 一项;第二页后达到读取预算,尚未遍历完,不能判断总共只有一项。”保留任务清单、范围、视图、已验收页数、请求数和原因,读者才能决定是否继续读取。

Google Drive 的 files.list 同时提供 nextPageToken 与 incompleteSearch。后者提示有文档未被搜索,说明没有后续页也不能忽略来源的不完整提示。本例用 search_complete 模拟这一条件:已返回的记录可作为部分发现,但报告只能是 partial。它没有连接 Drive,也没有声称 Drive 提供本例的固定快照字段。

尤其不要把“零条已观察记录”与“集合确实为空”写成一句话。权限失败、第一请求超时与正常完成的空列表可能都没有任务行,状态却不同。show 在没有已观察记录时打印 none observed,最终表述仍必须结合 status 与 reason。若后续让模型润色报告,应把这些状态作为不可丢失的输入。

离线应用的完成,不等于所有数据源都验证了

本程序使用类型明确的本地 Page 与 Task 对象,不是任意 JSON 的通用解析器;真实接入还要验证响应结构、设置网络超时并处理服务专属错误。它没有实现断点续读、重试、并行分片或真实服务的快照创建与释放。页预算能限制请求次数,单个远端请求会等待多久仍须另设超时。

本题把 open 当作来源已有的结构化状态。它不负责从会议纪要识别隐含承诺,也不证明人写的任务状态正确。产物能证明的是:在指定夹具与声明的列表契约下,收集过程怎样继续、停止、拒绝混合数据,并诚实保留 partial。交付时把这个边界与运行日志一起保存。

记住这三点

  • 先取得可遍历集合的明确契约,再讨论是否列全;搜索前几条不是同一种承诺。
  • 空页、估计总数和已经发现的数量都不能擅自代替协议的结束标志。
  • 完整性同时受授权范围、固定视图、来源覆盖与失败状态约束;部分结果也应清楚报告。

别把例子用过头

  • 示例 snapshot 字符串是自定义逻辑标识,不能直接替代真实服务可能变化的游标或 PIT 句柄。
  • 示例只处理受控对象与离线异常,没有验证真实网络、权限系统或服务端快照实现。
  • complete 仅针对明确集合与可信来源契约;它不证明所有现实待办都已被记录。

自己试一小步

如果同一次报告要读取两个已获授权的小组,其中一个读完、另一个超时,你会怎样设计结果?

  1. 先独立规定每个小组的范围、快照和预算,以及合并报告何时可以说完成。
  2. 设计一组跨小组相同任务 ID 的夹具,决定唯一标识应由哪些字段组成。
  3. 加入空页、一个来源的覆盖警告和一个来源的超时,保存报告及每个来源的证据。

做到这些,就算完成

  • 能否分别说明各小组读到了哪里,并保留已验证的部分发现?
  • 是否避免跨范围错误去重,也没有用成功小组掩盖失败小组?
  • 是否先检查权限与来源合同,再把集合完整性和现实事实完整性分开说明?

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • AIP-158:分页契约 (新标签页)↗Google API Improvement Proposals · 英文

    阅读 Guidance、Opacity;支持未到末尾也可返回空页、next_page_token 结束规则、参数保持、标记不透明与总数可能是估计。具体 API 仍以自身合同为准。

    2026-10-13 已打开原始正文并核对所列小节;公开阅读,无需登录。本文没有运行这些产品或真实模型。

  • Elasticsearch:分页、PIT 与固定查询 (新标签页)↗Elastic · 英文

    阅读 Search after 中刷新影响、PIT、排序唯一决胜字段及后续使用最新 PIT ID 的说明;仅作为真实服务稳定视图机制的例子。

    2026-10-13 已打开原始正文并核对所列小节;公开阅读,无需登录。本文没有运行这些产品或真实模型。

  • Google Drive:files.list 的覆盖提示 (新标签页)↗Google for Developers · 英文

    阅读 Response body 的 nextPageToken、incompleteSearch 与查询范围参数;支持分页结束仍需结合搜索覆盖提示。未访问任何用户文件。

    2026-10-13 已打开原始正文并核对所列小节;公开阅读,无需登录。本文没有运行这些产品或真实模型。