内置工具¶
内置工具都要显式导入和挂载。查看 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 一致:
下面以直接返回 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=。
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。
延伸阅读¶
- 工具:这些工具如何构建;你自己的工具也按同样方式写
- 工作区:文件和 shell 工具
- 示例:
tools/01_fetch.py,tools/02_time.py,tools/03_search.py,tools/04_human.py