来源只提供了标题,没有披露 Shippy 的架构、模型、工具集或运行环境。因此,本文不把具体实现归因于原项目,而是围绕“构建一个 Agent 会教会我们什么”这一主题,给出一套可以落地验证的工程方法。
Agent 与普通聊天应用的区别,不在于提示词更长,而在于模型能够观察环境、选择动作、调用工具,并根据结果继续决策。一旦系统进入这个循环,工程重点就从“生成一段好文本”转向“控制一个可能失败、重复或越权的执行过程”。
Agent 的核心是受约束的执行循环
一个实用 Agent 通常包含四个动作:读取当前状态、决定下一步、执行工具、检查结果。循环结束条件必须由宿主程序掌握,不能完全交给模型。
最小状态至少应记录:
- 用户目标与不可违反的约束
- 已执行的动作及其结果
- 剩余步数、时间或费用预算
- 当前产物与完成条件
- 最近一次错误以及重试次数
这解释了为什么只写一条“请自主完成任务”的提示词远远不够。没有步数上限,Agent 可能反复调用同一工具;没有完成条件,它可能过早宣布成功;没有结构化状态,长对话中的关键信息会逐渐丢失。
工具接口比工具数量更重要
工具是 Agent 与真实世界之间的边界。工具定义含糊时,模型必须猜测参数;返回值不稳定时,模型很难判断动作是否成功;工具权限过大时,一次错误决策就可能产生不可逆影响。
工具设计可以遵循几个约束:
- 输入使用明确的结构化字段,并在执行前校验。
- 输出同时包含机器可读状态和简短说明。
- 查询与修改操作分离,避免一个工具承担多种副作用。
- 写操作尽量支持预览、幂等键和回滚。
- 日志记录参数摘要、耗时、结果与错误,但过滤密钥和隐私数据。
例如,不要提供一个接收任意 Shell 字符串的 run_command。更稳妥的做法是暴露 list_files、read_file 或 run_tests 等窄接口,并由宿主程序限制路径和参数。
可以这样实践:实现一个可运行的最小 Agent
下面的示例不依赖真实模型 API。它用一个确定性的“规划器”模拟模型决策,重点展示工具白名单、参数校验、状态记录和执行预算。保存为 agent.py 后,可直接运行 python agent.py。
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
WORKSPACE = Path.cwd().resolve()
def list_files(path: str = ".") -> dict[str, Any]:
target = (WORKSPACE / path).resolve()
if target != WORKSPACE and WORKSPACE not in target.parents:
return {"ok": False, "error": "path escapes workspace"}
if not target.is_dir():
return {"ok": False, "error": "directory does not exist"}
return {
"ok": True,
"files": sorted(p.name for p in target.iterdir())[:20],
}
TOOLS: dict[str, Callable[..., dict[str, Any]]] = {
"list_files": list_files,
}
@dataclass
class AgentState:
goal: str
max_steps: int = 4
history: list[dict[str, Any]] = field(default_factory=list)
def planner(state: AgentState) -> dict[str, Any]:
# 在真实系统中,这里可以替换为要求返回 JSON 的模型调用。
if not state.history:
return {"type": "tool", "name": "list_files", "args": {"path": "."}}
result = state.history[-1]["result"]
if result.get("ok"):
return {
"type": "finish",
"answer": f"工作区包含:{', '.join(result['files']) or '没有文件'}",
}
return {"type": "finish", "answer": f"任务失败:{result['error']}"}
def run_agent(goal: str) -> str:
state = AgentState(goal=goal)
for _ in range(state.max_steps):
action = planner(state)
if action.get("type") == "finish":
return str(action.get("answer", ""))
name = action.get("name")
args = action.get("args", {})
tool = TOOLS.get(name)
if tool is None or not isinstance(args, dict):
result = {"ok": False, "error": "invalid tool request"}
else:
try:
result = tool(**args)
except (TypeError, ValueError) as exc:
result = {"ok": False, "error": str(exc)}
state.history.append({"action": action, "result": result})
return "任务因达到最大执行步数而终止"
if __name__ == "__main__":
print(run_agent("列出当前工作区中的文件"))
接入真实模型时,可以保留 run_agent 和工具层,只替换 planner。模型输出应使用 JSON Schema 或 SDK 提供的结构化输出能力,并限定为两种结果:调用已注册工具,或者提交最终答案。不要直接执行模型生成的 Python、SQL 或 Shell 文本。
评估不能只看最终答案
Agent 可能给出正确答案,却走了一条昂贵或危险的路径。因此,评估应同时覆盖结果和过程:
- 任务成功率:最终产物是否满足可自动检查的验收条件。
- 路径效率:调用了多少工具,是否存在重复或无效步骤。
- 恢复能力:工具超时、返回空数据或参数错误后能否调整。
- 权限合规:是否尝试调用未授权工具或访问工作区之外的资源。
- 成本与延迟:每个任务消耗的 token、模型调用次数和总耗时。
测试集不应只有理想输入。还要加入缺失参数、矛盾要求、不可达资源、工具返回脏数据和提示注入等场景。对于会发送消息、修改数据或部署代码的 Agent,测试环境必须使用隔离账号和可清理资源。
上线时逐级扩大自主权
Agent 的自主程度不是越高越好。一个稳妥的采用顺序是:先让它只生成计划,再允许调用只读工具,随后开放可预览的写操作,最后才考虑无需人工确认的执行。
上线前可以检查以下项目:
- 每个循环都有步数、时间和费用上限。
- 每个工具都采用最小权限,并校验输入与资源范围。
- 高风险动作需要人工批准,或者至少提供预览和撤销机制。
- 日志能够重建决策过程,同时不会泄露凭据。
- 完成条件可以由程序验证,而不是只相信模型的自我判断。
- 关键任务准备了确定性的降级路径。
构建 Agent 的难点通常不在于让模型“开始行动”,而在于让它知道何时停止、失败后如何恢复,以及哪些事情无论如何都不能做。把循环、状态、工具和评估设计成明确的工程边界,Agent 才能从演示进入可靠的生产流程。