在智能体开发领域,如何平衡功能扩展性和系统稳定性一直是个核心挑战。传统做法往往需要在添加新功能时修改核心逻辑,这不仅增加了维护成本,也容易引入新的错误。今天我要分享的正是解决这一痛点的设计模式——基于分发字典的插拔式工具系统。
这个方案最吸引我的地方在于它的简洁与强大。通过引入工具分发机制,我们实现了"新增工具只需添加处理函数和注册映射,无需触碰核心循环"的目标。这种设计完美体现了开闭原则(OCP)的精髓:对扩展开放,对修改关闭。
在基础版智能体中,我们仅通过bash命令与系统交互,这带来了几个显著问题:
cat命令可能截断大文件,sed遇到特殊字符容易崩溃解决方案的核心在于建立两层分离机制:
这种分离带来的最大好处是:核心循环可以保持稳定,而工具集可以灵活扩展。就像电脑的USB接口,你可以随时插入新设备而不用重装系统。
安全是工具系统的首要考虑。我们实现了safe_path函数来约束文件访问范围:
python复制def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
这个函数做了三件事:
../etc/passwd)立即抛出异常所有文件操作工具都会先调用这个函数进行安全检查,确保智能体不会越权访问系统文件。
系统内置了四种基础工具,每个都有特定的安全考量:
python复制def run_read(path: str, limit: int = None) -> str:
try:
text = safe_path(path).read_text()
lines = text.splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)[:50000]
except Exception as e:
return f"Error: {e}"
关键设计点:
python复制def run_write(path: str, content: str) -> str:
try:
fp = safe_path(path)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
特色功能:
python复制def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
fp = safe_path(path)
content = fp.read_text()
if old_text not in content:
return f"Error: Text not found in {path}"
fp.write_text(content.replace(old_text, new_text, 1))
return f"Edited {path}"
except Exception as e:
return f"Error: {e}"
安全特性:
这是整个系统的核心创新点:
python复制TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}
字典的妙处在于:
为了让LLM理解如何使用这些工具,我们需要提供元数据描述:
python复制TOOLS = [
{
"name": "bash",
"description": "Run a shell command.",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]
}
},
# 其他工具定义...
]
每个工具定义包含:
LLM根据这些描述决定调用哪个工具以及如何传递参数。
尽管功能大幅增强,核心循环却几乎保持不变:
python复制def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
print(f"> {block.name}: {output[:200]}")
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})
messages.append({"role": "user", "content": results})
关键改进点:
| 特性 | 基础版 | 多工具版 |
|---|---|---|
| 工具数量 | 1个(仅bash) | 可扩展的多工具 |
| 调用方式 | 硬编码 | 分发字典动态调用 |
| 路径安全 | 无 | 沙箱防护 |
| 核心循环 | 随功能修改 | 稳定不变 |
| 扩展成本 | 需修改核心代码 | 仅需注册新工具 |
bash复制python agents/s02_tool_use.py
s02 >> Read the file requirements.txt
> read_file: [文件内容]
s02 >> Create a file called greet.py with a greet(name) function
> write_file: Wrote 50 bytes to greet.py
s02 >> Edit greet.py to add a docstring to the function
> edit_file: Edited greet.py
s02 >> Read greet.py to verify the edit worked
> read_file: [修改后的文件内容]
这个交互过程展示了:
假设我们要添加一个代码格式化工具,只需三步:
python复制def run_format(path: str, style: str = "pep8") -> str:
try:
fp = safe_path(path)
code = fp.read_text()
# 实际格式化逻辑...
fp.write_text(formatted_code)
return f"Formatted {path} with {style} style"
except Exception as e:
return f"Error: {e}"
python复制TOOL_HANDLERS = {
# ...原有工具
"format_code": lambda **kw: run_format(kw["path"], kw.get("style")),
}
python复制TOOLS = [
# ...原有定义
{
"name": "format_code",
"description": "Format code file with specified style.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"style": {"type": "string", "enum": ["pep8", "google", "black"]}
},
"required": ["path"]
}
}
]
工具未被调用:
权限问题:
safe_path逻辑是否过于严格模型不理解工具:
上下文管理:
limit参数控制数据量工具组合:
缓存机制:
基础safe_path可以扩展更多安全检查:
python复制def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escape attempt: {p}")
if path.name.startswith('.'):
raise ValueError(f"Hidden file access denied: {p}")
if any(part.startswith('.') for part in path.parts):
raise ValueError(f"Hidden directory in path: {p}")
return path
新增防护:
记录所有工具调用详情:
python复制def log_tool_use(tool_name: str, params: dict, result: str):
entry = {
"timestamp": datetime.now().isoformat(),
"tool": tool_name,
"params": params,
"result": result[:1000] # 截断长结果
}
with open("tool_audit.log", "a") as f:
f.write(json.dumps(entry) + "\n")
在工具调用后添加日志记录:
python复制output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
log_tool_use(block.name, block.input, output)
这种插拔式设计为系统演进提供了良好基础:
我在实际项目中发现,这种架构特别适合快速迭代的场景。团队可以并行开发不同工具,只要遵守接口规范,就能无缝集成到系统中。