无需GPU:用ArcLM构建可复现的本地Agent工具

ArcLM 用分层设计与确定性假模型演示 Agent 工具调用的完整链路与安全边界。
文章以 ArcLM 的小型计算器 Agent 为例,演示 Agent 工具调用链路的完整实现。核心思路是把工具定义、工具注册表、模型适配器、运行器和安全策略分层:业务逻辑保持为普通 Python 代码,模型可替换而不影响工具本身。示例通过 @tool 装饰器配合类型注解与 permission、side_effect 元数据暴露可发现的 Schema;用 ToolRegistry 统一注册与检索;用确定性假模型适配器在无 GPU、无外部 API 的情况下跑通 Agent 循环;用 AgentRunner(max_steps=2) 限制步骤数防止无限循环。文章强调 Schema 不等于安全,声明副作用不等于强制审批,脱敏只检测已支持模板而不能替代密钥管理,并说明如何通过 trace 事件诊断执行过程。
AI Agent 真正有用的地方,不是它能生成多漂亮的文本,而是它能调用函数、校验参数、执行操作,并返回结构化结果。但一旦让 Agent 接触工具,工程问题就扑面而来:工具能接收哪些参数?如何防止 Agent 无限循环?怎么检查它调用了哪些工具?工具要改文件时权限怎么管?诊断日志里又如何不泄露敏感信息?
本文用一个可复现的小型计算器 Agent 演示 ArcLM 的工具调用链路。全程不需要 GPU,也不需要外部 LLM API,我们用一个确定性的假模型适配器跑通整条管线。需要强调的是,假模型只是测试适配器,用来验证工具调用机制,而不是展示 LLM 的推理能力。

为什么工具调用值得单独设计
很多 Agent 实现把能力定义、执行控制和安全策略混在一起,导致后期难以替换模型、难以审计、也难以限制风险。ArcLM 的思路是把这些职责拆开:工具定义能力,注册表管理可用工具,适配器负责模型交互,运行器控制执行边界,权限与脱敏处理各自的安全问题。
这种分层的直接好处是,业务逻辑可以保持为普通 Python 代码,不依赖 Agent 运行时。将来想把假模型换成真实模型,只需要替换适配器,不用重写工具本身。
定义一个带类型的工具
先看最核心的部分——一个简单的整数相加函数:
from arclm.agents import (
SideEffectLevel,
ToolPermission,
tool,
)
@tool(
category="utilities",
permission=ToolPermission.PURE,
side_effect=SideEffectLevel.NONE,
)
def add_numbers(left: int, right: int) -> int:
"""Add two integers."""
return left + right
实现只有几行,但元数据很关键。@tool 装饰器把函数暴露为 ArcLM 工具,类型注解描述参数和返回类型,docstring 充当描述,category 用于分组。
权限和副作用声明描述了操作行为:ToolPermission.PURE 表示不需要访问外部资源,SideEffectLevel.NONE 表示没有副作用。函数本身仍是普通 Python,ArcLM 只在外面包了一层工具抽象。
SideEffectLevel 描述的是工具执行后对外部世界造成的影响能否被撤销,常见分级包括 NONE(纯计算,不改动任何外部状态)、REVERSIBLE(可撤销,例如写入一个可回滚的临时文件)以及不可逆或外部可见的操作。它与 permission 是两个互相独立的维度:权限回答"这个工具需要访问什么资源",副作用回答"访问之后会留下什么后果"。一个工具可能只需读取本地文件(低权限)却把内容发送到网络(高影响),也可能拥有高权限但副作用可完全回滚。把这两者分开声明,运行器和审批策略才能分别判断是否需要授权、是否需要人工确认。
注册工具并检查 Schema
ArcLM 通过 ToolRegistry 按名称管理工具:
from arclm.agents import ToolRegistry
registry = ToolRegistry()
registry.register_tool(add_numbers)
registered_tool = registry.get_tool("add_numbers")
print(registered_tool.name)
print(registered_tool.description)
print(registered_tool.input_schema)
注册表提供了统一的检索和元数据检查方式,input_schema 描述了期望的参数,模型适配器可以据此决定如何构造工具调用。这比直接把任意 Python 函数丢给模型要明确得多。
但要注意,Schema 能描述函数接受什么,并不证明工具安全。参数校验和副作用访问控制仍然必须单独处理。ArcLM 的工具抽象在调用底层函数前会做参数校验。
JSON Schema 是一套用于描述 JSON 数据结构的规范,通过 type、properties、required、enum、minimum 等关键字约束数据形状。在工具调用场景中,模型适配器把每个工具的 input_schema 转成模型 API 能理解的函数签名,模型据此生成符合结构的参数对象,运行器再用同一份 Schema 校验模型实际返回的参数是否合法。这样做的意义在于把"模型生成"和"参数合法"解耦:模型只负责按结构填值,合法性由确定性的校验逻辑保证,避免非法输入直接进入业务函数。需要留意的是,Schema 校验的是结构与类型,无法表达"这个路径是否允许写入"这类策略性约束,后者必须交给权限与审批层。
用确定性假模型跑通 Agent 循环
真实模型会自己决定何时调用 add_numbers、传什么参数。这引入了外部依赖,也可能带来不确定性。为了可复现,我们实现一个极简假模型:第一次返回工具调用请求,第二次读取结构化结果并给出最终回复。
完整示例(保存为 calculator_agent.py)如下:
import json
from arclm.agents import (
Agent,
AgentRunner,
ModelResponse,
SideEffectLevel,
ToolCall,
ToolPermission,
ToolRegistry,
tool,
)
from arclm.security import redact_sensitive_text
模型适配器是 Agent 框架与具体 LLM 供应商之间的抽象层,负责把框架内部的会话、工具定义转换成供应商 API 所需的请求格式,再把返回结果转回框架的 `ModelResponse` 结构。一个适配器通常需要实现类似 `generate_response(messages, tools, instructions, session)` 的方法,使运行器不必关心底层是 OpenAI、本地模型还是测试用的假实现。本文的 `DeterministicFakeModel` 正是这样一个适配器:它忽略模型推理,按调用次数返回预设的工具调用或最终回复。这种可替换设计让同一套工具、权限和 trace 机制能在无网络、无 GPU 的条件下被完整测试,也方便在切换到真实模型时复用全部业务代码。
# 1. Define a typed tool.
@tool(
category="utilities",
permission=ToolPermission.PURE,
side_effect=SideEffectLevel.NONE,
)
def add_numbers(left: int, right: int) -> int:
"""Add two integers."""
return left + right
# 2. Register the tool.
registry = ToolRegistry()
registry.register_tool(add_numbers)
# 3. Create a deterministic fake model adapter.
class DeterministicFakeModel:
def __init__(self):
self.calls = 0
def generate_response(
self,
*,
messages,
tools,
instructions,
session,
):
self.calls += 1
if self.calls == 1:
return ModelResponse(
tool_calls=[
ToolCall(
id="call_1",
name="add_numbers",
arguments={
"left": 2,
"right": 5,
},
)
]
)
# Read the actual result returned by the tool.
for message in reversed(messages):
if message.role == "tool":
result = json.loads(message.content)
if result["success"]:
return ModelResponse(
content=f"The sum is {result['value']}."
)
return ModelResponse(
error_code="tool_failed",
error_message="The calculator tool failed.",
)
return ModelResponse(
error_code="missing_tool_result",
error_message="No calculator result was received.",
)
# 4. Configure and run the agent.
agent = Agent(
name="calculator",
instructions="Use the available calculator tool to add integers.",
model=DeterministicFakeModel(),
tools=registry.list_tools(),
)
runner = AgentRunner(max_steps=2)
result = runner.run(
agent,
"Add 2 and 5.",
)
if not result.success:
raise RuntimeError(
f"Agent run failed: {result.error_code}"
)
# 5. Inspect the result and execution trace.
report = {
"success": result.success,
"output": result.output,
"tool_calls": [
call.name for call in result.session.tool_calls
],
"trace_events": [
event.event_type for event in result.trace.events
],
}
print(json.dumps(report, indent=2))
# 6. Demonstrate sensitive-text redaction.
sample = "api_key=demo-secret"
print(redact_sensitive_text(sample))
运行 python calculator_agent.py 后,你会看到一次成功的计算器运行,以及一次脱敏演示。建议在自己的 ArcLM 版本上跑一遍并记录真实输出。
Agent 循环内部发生了什么
执行顺序大致是:运行器把对话和可用工具描述发给假模型;模型返回结构化的 ToolCall;运行器检查工具是否存在、是否被允许;ArcLM 校验参数并调用函数;工具结果写入会话;假模型读取结果并返回最终回复。
注意第二次回复是从工具返回值推导出来的,而不是硬编码数字 7,这正是这个示例的价值所在——它让你能清楚看到 Agent、工具与模型适配器之间的交互。
为什么要限制 Agent 循环
AgentRunner(max_steps=2) 这行很关键。一个反复请求工具却始终不给出最终答案的 Agent,如果不加约束,会无限制消耗资源。max_steps 限制了模型响应步骤数,本示例两步足够:一次工具请求加一次最终回复。
但限制并不保证一定成功。模型可能请求不存在的工具、返回非法响应或始终给不出答案。ArcLM 用结构化结果对象表示执行结果,调用方可以检查成功与失败,而不必只依赖打印文本。
权限与副作用控制
计算器是一个好的起点,因为它不修改外部状态。文件写入工具则完全不同,它可能覆盖数据、在意外位置创建文件,或把信息暴露给其他进程。
ArcLM 把权限声明和副作用分类分开。例如一个写入本地工作区的工具可以这样声明:
from arclm.agents import (
SideEffectLevel,
ToolPermission,
tool,
)
@tool(
category="filesystem",
permission=ToolPermission.WRITE_LOCAL,
side_effect=SideEffectLevel.REVERSIBLE,
)
def write_report(path: str, content: str) -> str:
"""Write a report to an approved workspace location."""
# Implement explicit path validation and file-writing policy here.
...
这只是分类示例,不是文件写入实现。生产工具还需要校验目标位置、限制允许路径、处理错误并定义覆盖行为。
两个字段的区分很重要:permission 声明工具需要哪类访问权限,side_effect 描述操作后果。需要特别强调:仅声明副作用并不会强制执行审批。ArcLM 的审批策略使用工具的权限分类和配置的 ToolPolicy。默认情况下,WRITE_LOCAL、READ_NETWORK、EXTERNAL_ACTION 和 SENSITIVE 等权限需要审批,而默认审批处理器会拒绝需要审批的操作,除非配置了合适的处理器。
真实应用中应通过 AgentRunner(approval_handler=...) 提供 ToolApprovalHandler,并定义审批如何请求、授予、拒绝和审计。绝不要把描述性的工具 Schema 当作访问控制的替代品。
脱敏:处理敏感信息
工具输出和执行日志可能包含凭证、令牌等敏感值。ArcLM 提供 redact_sensitive_text:
from arclm.security import redact_sensitive_text
sample = "api_key=demo-secret"
safe_text = redact_sensitive_text(sample)
print(safe_text)
预期输出是 api_key=[REDACTED]。当 Agent 安全策略启用 trace 脱敏时,ArcLM 也会对工具结果负载进行脱敏。
但脱敏有局限。它只能检测支持的模板,无法保证识别所有秘密或个人信息。还要区分工具结果脱敏和完整会话清理——会话会记录工具调用参数,不要因为启用了结果脱敏就真的把凭证当作工具参数传进去。敏感工作流应遵循:避免把秘密放进提示和工具参数;凭证放在专门的密钥管理机制中;最小化诊断输出内容;用应用中的真实样例测试脱敏效果。
脱敏(redaction)与密钥管理是两件互补但不等价的事。脱敏是在文本已经产生之后,用模式匹配把疑似敏感片段替换成占位符,属于输出侧的最后一道防线,漏检在所难免。密钥管理则是在源头避免明文出现,例如通过环境变量、密钥管理服务或短时令牌获取凭证,让密钥从不进入提示词、工具参数和会话记录。合理的做法是两者叠加:凭证通过密钥机制注入并在使用后立即丢弃,日志和 trace 再经脱敏处理。此外要注意会话会持久记录工具调用参数,因此即使启用了结果脱敏,也不应把密钥作为参数传给工具——脱敏只覆盖已识别模板,不能保证清除会话中的全部敏感内容。
检查执行 Trace
最终答案告诉你 Agent 返回了什么,而 trace 帮你解释它是怎么走到这个结果的。示例通过 result.trace.events 收集事件名,运行器会记录 run_started、model_responded、tool_requested、tool_started、tool_completed、step_completed、run_completed 等事件。
这些事件能回答实际调试问题:模型请求工具了吗?运行器启动它了吗?它完成了吗?Agent 给出了最终回复,还是耗尽了步骤上限?
更大规模的应用里,还可以进一步检查结构化 trace 和结果元数据。但在持久化 trace 前,先想清楚:哪些信息该保留、谁可以访问、敏感数据如何处置。
这个示例真正演示了什么
这个小组件把常被混在一起的职责拆开了:工具定义能力,带类型函数暴露可发现的 Schema;注册表管理可用工具,支持显式注册和选择;适配器定义模型交互,确定性适配器无需 Provider 即可演示协议;运行器控制执行,校验工具调用、协调结果并限制步骤数;权限和脱敏处理不同的安全问题,权限管访问与审批,脱敏降低已识别敏感值的暴露。
它不是生产级自主 Agent,也不展示真实语言模型推理。它是一个小而可检查的基础,帮助你理解工具调用系统如何拼在一起。下一步实验是把假适配器换成真实模型适配器,同时保留相同的工具边界、步骤限制、权限策略和 trace 检查机制。这种可替换性,才是这套设计最有价值的地方。
相关推荐

Workbuddy零基础入门:桌面AI智能体如何落地工作流
Workbuddy桌面AI智能体零基础教程解读:从安装、积分领取到文件管理与自动化工作流搭建,对比CodeX的适用场景与成本差异,帮助职场人完成从AI提问者到AI管理者的思维转变。

OpenAI智能体新栈:计算机操作、决策API与超速工作流
OpenAI Dev Day发布智能体新栈:Computer Use操作速度超越普通人,Decisions API基于Luna实现极低延迟分类,Agents API开放同源计算机操作能力,GPT-6.1 Sol成本仅为Astra七分之一。本文梳理核心更新、实测数据与开发者落地要点。

梁文峰开源推理框架DeepInfo,硬刚美国禁令
DeepSeek在开源模型后再开源推理框架DeepInfo,包含GPU/CPU交叉流水线、小模型预判大模型复核、华为昇腾与英伟达芯片全面适配等技术,直面美国联邦禁令。本文解析其对AI产业落地的意义与开源策略。