跳转至

主动追问

用户让 Agent 写报告,却没有说明目标读者。Agent 可以先提问,拿到答案后再继续。用 SDK 注册一个自定义 ask_user 工具即可实现。

提问并等待回答

完成模型配置,运行:

python examples/runtime/ask_user.py
"""最小追问:等待回答;输入 /skip 跳过。"""

import asyncio
import os

import bumblehive


async def read_answer(question: str, options: list[str] | None = None) -> dict:
    print(f"\n问题:{question}")
    choices = options or []
    for index, option in enumerate(choices, 1):
        print(f"  {index}. {option}")
    while True:
        answer = (await asyncio.to_thread(
            input, "输入答案或选项编号;/skip 跳过:"
        )).strip()
        if answer == "/skip":
            return {"status": "skipped"}
        if not answer:
            continue
        if answer.isdecimal() and 1 <= int(answer) <= len(choices):
            answer = choices[int(answer) - 1]
        return {"status": "answered", "answer": answer}


async def main() -> None:
    config = bumblehive.RuntimeArguments(
        model=os.environ["BUMBLEHIVE_MODEL"],
        api_key=os.environ["BUMBLEHIVE_API_KEY"],
        base_url=os.environ["BUMBLEHIVE_BASE_URL"],
        skill_names=[],
        tool_names=["ask_user"],
    )
    async with bumblehive.from_config(config) as runtime:
        @runtime.tools.tool(
            description=(
                "缺少无法从上下文确定的关键信息时,向用户提出一个简短问题。"
                "可提供建议选项,用户也能自由输入。"
                "answered 包含回答;skipped 表示用户跳过,不要重复提问或当作同意。"
            ),
        )
        async def ask_user(question: str, options: list[str] | None = None) -> dict:
            return await read_answer(question, options)

        result = await runtime.run(
            "帮我写一段介绍 Python 的开场白。请先用 ask_user 询问目标读者,"
            "收到回答后再写;如果用户跳过,就写一个通用版本。"
        )
        print(result.error.message if result.error else result.final_content)


if __name__ == "__main__":
    asyncio.run(main())

工具描述告诉模型什么时候提问;read_answer() 接收用户输入;工具把答案返回给模型。本例一直等待,直到用户回答或输入 /skip。

运行效果

问题:这段 Python 开场白面向哪些读者?
  1. 没有编程经验的初学者
  2. 有其他语言经验的开发者
输入答案或选项编号;/skip 跳过:1
…Agent 为初学者写出开场白…

问题和选项由模型生成,实际措辞可能不同。输入编号会选择对应答案,也可以直接输入任何文字。不传 options 时只显示输入提示。输入 /skip 返回 skipped,Agent 按任务要求继续或说明缺少什么信息,不把跳过当成同意。

用户没有立即回答怎么办

不希望一直等待时,可以设置等待时长。这里复用工具的等待与后台执行,不需要另写一套超时机制。

从仓库根目录运行以下进阶示例。它会导入上面的 read_answer() 和公共示例辅助代码:

python -m examples.runtime.ask_user_timeout
"""从仓库根目录运行:python -m examples.runtime.ask_user_timeout"""

import asyncio
import os

import bumblehive
from examples.runtime.ask_user import read_answer
from examples.runtime.background_support import BackgroundTools, Conversation


WAIT_SECONDS = 60


async def main() -> None:
    config = bumblehive.RuntimeArguments(
        model=os.environ["BUMBLEHIVE_MODEL"],
        api_key=os.environ["BUMBLEHIVE_API_KEY"],
        base_url=os.environ["BUMBLEHIVE_BASE_URL"],
        skill_names=[],
        tool_names=["ask_user"],
    )
    conversation = Conversation()

    def on_answer(task_id: str, value: dict) -> None:
        # 超时后跳过只结束等待,不再给 Agent 发送消息。
        if value["status"] != "skipped":
            conversation.notify(task_id, value)

    background = BackgroundTools(on_answer)
    async with bumblehive.from_config(config) as runtime:
        @runtime.tools.tool(description=(
            "缺少关键信息时询问用户,可提供建议选项,也允许自由输入。"
            f"等待 {WAIT_SECONDS} 秒。answered 包含答案;skipped 表示跳过,不要重复问。"
            "pending 表示还未回答,晚到答案会带原问题作为消息送达。"
            "可以继续独立工作;必须依赖答案时说明阻碍并结束本轮。"
            "pending 和 skipped 都不代表同意,不要猜答案或重复提问。"
        ))
        async def ask_user(question: str, options: list[str] | None = None) -> dict:
            async def read() -> dict:
                return {"question": question, **await read_answer(question, options)}

            return await background.submit(read(), wait_seconds=WAIT_SECONDS)

        await conversation.run(
            runtime,
            "帮我写介绍 Python 的开场白,先用 ask_user 询问目标读者。"
            "等待时可以先列出通用提纲;收到答案再写正文。"
            "如果跳过,就只给通用提纲。不要重复提问。",
            background,
        )


if __name__ == "__main__":
    asyncio.run(main())

修改 WAIT_SECONDS 调整等待时间,例如改为 5 方便体验。这里的等待时长和返回状态都是示例定义的,不是 SDK 内置配置。

用户操作 Agent 收到什么
等待期间回答 工具直接返回 answered 和答案
等待期间跳过 工具直接返回 skipped
等待超时 工具返回 pending,输入仍然有效
超时后回答 原问题和答案作为后续消息送达
超时后跳过 结束提问,不再发送消息

例如先不回答,让等待超时:Agent 可以先给通用提纲。随后输入“初学者”,它会结合回答继续。运行中收到答案用 steer();本轮已经结束则用原历史开始下一轮,具体代码集中在后台工具示例。

pending 表示尚未回答。如果剩下的工作必须依赖答案,Agent 应说明阻碍并结束本轮;应用继续等待用户,不要猜测答案或反复提问。

这个终端示例使用一个输入口,适合一次回答一个问题。input() 在线程中等待,取消协程不能终止终端读取;退出前输入答案或 /skip。接入界面时,把 read_answer() 换成等待该问题提交事件的异步函数即可。