第 15 章:造一把新工具

个人公众号
1
2
卷三:工坊实战
[15] 造新工具 <- you are here

前两卷我们追踪了请求、理解了设计。这一卷开始动手——从零造出 QwenPaw 的扩展组件。第一个任务:写一个新的内置工具。


目标

造一个 http_request 工具——让 Agent 能发 HTTP 请求获取网页内容。功能:接收 URL,返回页面内容(截断后)。这个工具足够简单来学习模式,又足够真实来理解工程约束。

工具的统一契约

每个 QwenPaw 工具都遵循相同模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async def tool_name(param1: str, param2: Optional[int] = None) -> ToolResponse:
"""一句话描述。

详细说明什么场景下 LLM 应该调用这个工具。

Args:
param1 (`str`): 参数说明。
param2 (`int`, optional): 参数说明。默认 None。
"""
try:
# 主逻辑
return ToolResponse(content=[TextBlock(type="text", text=result)])
except Exception as e:
return ToolResponse(content=[TextBlock(type="text", text=f"Error: {e}")])

四个关键要素:

  1. async 函数——所有工具都是异步的
  2. 类型注解——自动转换为 JSON Schema 告诉 LLM 参数类型
  3. Google 风格 docstring——自动解析为工具描述和参数说明
  4. 返回 ToolResponse——统一的返回格式

第一步:创建工具文件

src/qwenpaw/agents/tools/ 下创建 http_request.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import logging
from typing import Optional

import httpx
from agentscope.message import TextBlock
from agentscope.tool import ToolResponse

logger = logging.getLogger(__name__)

DEFAULT_MAX_BYTES = 50 * 1024 # 50KB,和文件工具一致

async def http_request(
url: str,
method: str = "GET",
max_length: Optional[int] = None,
) -> ToolResponse:
"""Send an HTTP request and return the response body.

Use this tool when you need to fetch content from a URL,
such as web pages or API endpoints.

Args:
url (`str`): The URL to request.
method (`str`, optional): HTTP method. Defaults to "GET".
max_length (`int`, optional): Max response bytes. Defaults to 50KB.
"""
if not url.startswith(("http://", "https://")):
return ToolResponse(
content=[TextBlock(type="text",
text="Error: URL must start with http:// or https://")]
)

max_bytes = max_length or DEFAULT_MAX_BYTES

try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.request(method, url)
resp.raise_for_status()
body = resp.text

if len(body.encode()) > max_bytes:
body = body.encode()[:max_bytes].decode(errors="replace")
body += f"\n\n... truncated ({max_bytes} bytes)."

return ToolResponse(content=[TextBlock(type="text", text=body)])

except httpx.HTTPError as e:
return ToolResponse(
content=[TextBlock(type="text",
text=f"Error: HTTP request failed: {e}")]
)

关键设计决策:

  • URL 验证:只允许 http/https 协议,防止 file:// 等协议滥用
  • 输出截断:默认 50KB,和文件工具一致——防止大响应撑爆上下文
  • 超时控制:30 秒超时,防止长时间等待
  • 错误处理:网络错误返回 Error 前缀的 ToolResponse,不抛异常

第二步:注册工具

src/qwenpaw/agents/tools/__init__.py 中导出:

1
from .http_request import http_request

src/qwenpaw/agents/react_agent.py 中注册:

1
2
3
4
5
6
7
8
9
10
11
# 1. 添加 import(约第 38 行)
from .tools import (
# ... 已有的 import ...
http_request,
)

# 2. 添加到 tool_functions 字典(约第 233 行)
tool_functions = {
# ... 已有的工具 ...
"http_request": http_request,
}

完成。LLM 会自动看到新工具的 JSON Schema,知道可以调用 http_request(url=..., method=...).

Docstring 怎么变成 JSON Schema

注册时,agentscope 的 Toolkit 自动解析 docstring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# agentscope 内部逻辑
schema = {
"type": "function",
"function": {
"name": "http_request",
"description": "Send an HTTP request...",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to request."
},
"method": {
"type": "string",
"description": "HTTP method."
},
"max_length": {
"type": "integer",
"description": "Max response bytes."
}
},
"required": ["url"]
}
}
}

methodmax_length 有默认值,所以不在 required 里。LLM 只需要提供 url

安全考虑

http_request 工具涉及网络访问,有潜在风险。注册后,它自动受 ToolGuard 系统保护(第 13 章)。如果需要额外检查,可以在 security/tool_guard/rules/ 下加一条 YAML 规则:

1
2
3
4
5
6
7
8
9
- id: TOOL_HTTP_INTERNAL_NETWORK
tools: [http_request]
params: [url]
category: unauthorized_access
severity: HIGH
patterns:
- "https?://(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.)"
description: "Requests to private/internal networks"
remediation: "Only public URLs are allowed"

验证

启动 QwenPaw,让 Agent 调用新工具:

1
2
3
用户:帮我获取 https://example.com 的内容
Agent:(调用 http_request(url="https://example.com"))
Agent:这是 https://example.com 的内容:...

如果 Agent 没有调用工具,检查 docstring 格式是否正确——Args: 必须用反引号包裹类型。

工程要点

  • 输出截断是必须的——LLM 上下文有限,50KB 是经过验证的安全阈值
  • 错误返回 ToolResponse 而非抛异常——Agent 能看到错误信息并自行重试
  • 类型注解要精确——Optional[int] 而非 int | None = None,保证 Schema 生成正确
  • 不要在代码里写注释解释实现——工具逻辑对 LLM 不可见,LLM 只看 Schema

自检

  • 创建了 tools/http_request.py,遵循 async + ToolResponse 模式
  • tools/__init__.py 导出了函数
  • react_agent.pytool_functions 字典注册了工具
  • 理解了 docstring 自动转换为 JSON Schema 的机制

下一章我们造一个新技能——不是函数调用,而是通过 Markdown 文档扩展 Agent 的知识。