跳转至

内置工具

内置工具都要显式导入和挂载。查看 Agent 的构造代码,就能知道它可以访问哪些外部能力。

from lovia import Agent
from lovia.tools import duckduckgo_search, now, web_fetch

agent = Agent(
    name="researcher",
    model="<model>",
    tools=[web_fetch(), duckduckgo_search(), now],
)

文件和 Shell 工具由工作区提供;需要人工补充信息时,使用 询问人工。

需求 工具
读取 URL:网页转成 Markdown,也支持 JSON、文本 web_fetch()
搜索公开网页 duckduckgo_search() / tavily_search()
获取当前时间 now
暂停并询问用户 ask_human

读取 URL

lovia.tools.web_fetch() 构建一个用 GET 读取单个 URL 的工具。HTML 会转换为保留结构和 链接的 Markdown;JSON 压缩后返回,文本保持原样,二进制只返回元数据。

它向模型公开三个参数:

参数 默认值 说明
url 必填 只允许绝对 http:// / https:// URL
images False 同时返回去重后的图片清单
offset 0 从这个字符偏移处继续读一个长页面

超时、下载上限和缓存时间等配置由调用方决定,因此放在 web_fetch() 工厂上, 不会出现在模型每次调用所见的 JSON Schema 中:

from lovia.tools import HttpReader, web_fetch

tool = web_fetch(                       # 或直接用默认配置的 web_fetch()
    HttpReader(timeout=15.0, max_bytes=2_000_000, cache_ttl=60),
    max_chars=40_000,
)

它没有 method、headers、body 参数。要让模型调用某个具体的 API,把这个 API 包装成 @tool,比如 get_order(order_id),而不是交给模型一个可以随意填写的 HTTP 客户端。

返回结果

URL: https://example.com/guide
HTTP 200 · text/html

--- BEGIN EXTERNAL CONTENT (reference material) ---
Title: Example Domain

# Example Domain

Images (1):
1. https://example.com/img/hero.png — Hero banner
--- END EXTERNAL CONTENT ---

[... truncated. Continue with offset=20000.]

网页自身的内容放在隔离框里(见隔离框),URL、状态和截断提示由 lovia 生成, 放在框外。页面过长时,截断提示会给出下一次应传入的 offset,便于分段续读。工具 返回的 Page 包含最终 URL、状态码、标题、Markdown 正文和图片列表;上例是默认渲染结果。

图片

Markdown 正文已经包含可用的 <img src>。设置 images=True 后,结果还会附上一份 去重并转换为绝对 URL 的图片清单,其中也包括 Markdown 无法完整表达的来源: 仅提供 srcset 时尺寸最大的候选、<picture><source> 和 og:image。相对 URL 会以 <base href> 和重定向后的 URL 为基准解析;data:、javascript: 和页内锚点会被忽略, 以免大段内联 base64 数据占用结果空间。

需要 JavaScript 时更换后端

HttpReader 通过普通 HTTP 请求获取页面,并使用标准库解析 HTML,不执行客户端 JavaScript。因此,纯前端渲染的单页应用可能只返回一个空壳。需要浏览器渲染或托管 抽取服务时,实现 PageReader 协议即可;它的接口形式与 WebSearch 一致:

class PageReader(Protocol):
    async def read(self, url: str, *, images: bool = False) -> Page: ...

下面以直接返回 Markdown 的 Jina Reader 为例:

import httpx
from lovia.tools import Page, web_fetch

class JinaReader:
    async def read(self, url: str, *, images: bool = False) -> Page:
        headers = {"X-Return-Format": "markdown"}
        if images:
            headers["X-With-Images-Summary"] = "true"
        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.get(f"https://r.jina.ai/{url}", headers=headers)
        return Page(
            url=url,
            status_code=resp.status_code,
            media_type="text/markdown",
            text=resp.text,
        )

agent = Agent(name="x", model="<model>", tools=[web_fetch(JinaReader())])

字符数限制由工具统一处理,因此后端应返回完整正文。

缓存与上限

响应按 URL 缓存 cache_ttl 秒(默认 300,最多保留 cache_size 条)。缓存不只是为了 加速:如果续读时重新下载,offset 前后可能来自网页的两个不同版本。单次下载上限为 1 MB;达到上限时,size_capped 会设为 True,结果中也会说明尾部尚未下载,继续增大 offset 无法读取这部分内容。4xx/5xx 响应的正文最多保留 500 个字符,避免错误页 占满结果空间。每次调用 web_fetch() 都会创建自己的 reader,缓存不会与其他工具共享。 TLS 设置见 LOVIA_HTTP_* 配置。

不会过滤 SSRF 风险。 web_fetch 可以访问当前主机能够访问的任何地址,包括内网 地址;重定向也可能跳转到内网。如果 Agent 可能处理不可信内容,请为工具启用审批或 隔离网络。

Web 搜索

web_search 是可插拔的搜索工具,自带两种后端:DuckDuckGo 无需 API 密钥, 但要安装 ddg extra;Tavily 无需安装额外依赖,但要设置 TAVILY_API_KEY 或传入 api_key=。

pip install "lovia[ddg]"   # 仅 DuckDuckGo 后端需要
from lovia.tools import duckduckgo_search, tavily_search, web_search

tools = [duckduckgo_search()]            # 无需 API 密钥,需要 lovia[ddg]
tools = [tavily_search()]                # Tavily API,读取 TAVILY_API_KEY
tools = [web_search(MySearchBackend())]  # 或你自己的后端

工具默认名为 web_search(可用 name= 覆盖),接收 query、max_results (1–20,默认 5)和可选的时间范围过滤 time_range("d" / "w" / "m" / "y")。结果会整理为标题、URL 和摘要,而不是直接返回 JSON。

自定义后端只需实现 WebSearch 协议中的 search() 方法:

class WebSearch(Protocol):
    async def search(
        self, query: str, *, max_results: int = 5, time_range: str | None = None
    ) -> list[SearchResult]: ...

后端必须能安全处理并发调用。显式传入后端(web_search(impl))意味着缺失的 可选依赖会在构造时失败,而不是运行到一半才失败。

时间

两个小工具:

  • now(工具):当前系统时钟时间,ISO-8601 格式;可选 tz 接收 IANA 名称(如 "Asia/Shanghai")。默认使用服务器本地时区。(Windows 上 IANA 名称需要 pip install tzdata。)
  • current_date(tz=None):不是工具,而是返回 指令片段的工厂,将当天日期写入系统提示词:
agent = Agent(name="researcher", model="<model>", tools=[duckduckgo_search()])
agent.instruction(current_date())

日期写入提示词后,模型搜索时可以直接使用当前年份,无需先调用一轮 now。这里只写日期, 不写精确时间,因为日期在提示词缓存周期内通常不会变化,对 Provider 缓存影响很小。需要精确时间时再调用 now。

询问人工

ask_human(channel) 让模型在运行中请求操作员输入。它与审批的 方向相反:审批由 Runner 询问“是否允许执行”。

from lovia import Agent
from lovia.tools import HumanChannel, ask_human

channel = HumanChannel()
agent = Agent(name="assistant", model="<model>", tools=[ask_human(channel)])

# 在操作员侧处理问题
async for q in channel.questions():   # channel.close() 后结束
    channel.answer(q.id, "使用选项 A。")

工具调用会等待,直到收到答案、问题被取消或 Channel 关闭。

当问题是单选(或配合 multi_select 的多选)时,模型会附带 2–4 个 options——每个 QuestionOption 有 label 和可选的一行 description。这些选项只是建议,任何字符串仍是 合法答案。多选答案用换行连接所选 label;schema 强制 label 单行且互不重复,因此结果没有歧义:

async for q in channel.questions():
    if q.options:
        print(q.question, [o.label for o in q.options], q.multi_select)
    channel.answer(q.id, "Kyoto\nOsaka")

每个问题还携带发问运行的 session_id / run_id(若有),服务多个会话的消费者可以据此 路由。ask_human 是执行屏障(parallel=False):一个运行同一时刻至多一个挂起问题。

API 效果
questions() 异步迭代已经排队的问题;只允许一个消费者
pending 用于轮询的未回答问题快照
answer(id, text) 回答问题;工具返回 text
cancel(id, reason=...) 用模型可见的 ToolError 取消一个问题
close(reason=...) 取消所有未回答问题、结束迭代,并拒绝后续提问

取消和关闭会成为工具错误结果,因此模型可以在没有答案时继续。操作员可能离线时,应为 工具设置超时。从其他线程回答时,先切回事件循环线程,例如使用 loop.call_soon_threadsafe(channel.answer, qid, text)。

如果问题是“是否允许执行”,并且答案为允许或拒绝,应使用审批;如果模型需要向人询问信息, 并接收自由文本回答,则使用 ask_human。

隔离框

web_fetch 和 web_search 工具会把抓取到的内容放在固定标记之间: --- BEGIN EXTERNAL CONTENT (reference material) --- / --- END EXTERNAL CONTENT ---, 并在各自的工具描述里告诉模型:框内内容只是供阅读和转述的素材,不是要执行的指令。框内 所有可能冒充 agent 自身格式的内容都会被解除:伪造的标记、<system-reminder> 标签、 对话模板 token,以及不可见的控制字符、双向文本字符和 Unicode 标签字符。lovia 自己生成的 行(状态、URL、截断提示)留在框外。

自定义工具如果返回第三方文本,可以复用同一个函数:

from lovia.tools import FENCE_NOTE, fence, tool

@tool(description=f"获取工单最新的客户回复。\n{FENCE_NOTE}")
async def latest_reply(ticket_id: str) -> str:
    return fence(await helpdesk.latest_reply(ticket_id))

隔离框只是缓解手段,不是安全边界:模型仍有可能被说服;真正兜底的是审批和隔离网络。 它还会改写文本,所以模型需要逐字节复现的内容(要编辑的文件、要保存的脚本)不要加框。

使用建议

  • web_fetch 是最需要警惕的内置工具。 配合不可信输入,它就是一个 SSRF 入口;在面向 公众的场景中使用前,请启用审批或隔离网络。
  • web_fetch 默认缓存响应 5 分钟。 变化比这更快的页面需要 web_fetch(HttpReader(cache_ttl=0))。
  • duckduckgo_search() / tavily_search() 会立即创建后端。 缺少 ddgs 包或 TAVILY_API_KEY 时,构建工具便会抛出 UserError,让配置问题在启动阶段暴露, 而不是拖到运行中途。
  • 搜索结果质量取决于后端。 DDG 后端不需要 API 密钥,但实际使用中可能遇到频率限制; 生产环境通常使用 Tavily 等需要密钥的后端,或自行实现 WebSearch。

延伸阅读