

























很多人第一次听到“Agent”这个词时,最容易把它理解成“更聪明的聊天机器人”。这个理解只对了一半。Agent 确实建立在大模型之上,但它的重点不是“会说”,而是“会做”。它不是只回答你一句话,而是能围绕一个目标持续推进:拆解任务、调用工具、观察结果、修正计划、保存状态,最后把事情做完。
如果把普通聊天模型比作“会表达的脑子”,那 Agent 更像“带着记忆和手脚的执行系统”。它不只回答“这是什么”,还会处理“去完成这件事”。这也是为什么越来越多的开发者开始自己实现 Agent 助手,而不是只停留在一个聊天框里。
本文将从工程实践视角出发,深入解析 Agent 助手的核心原理与系统架构,帮助读者理解一个 Agent 如何完成感知、决策、记忆与执行。通过架构拆解、模块分析以及代码示例,展示如何以最小成本构建一个可扩展、可演进的 Agent 原型,并为后续的复杂场景落地提供参考。
你可以把本文理解成一份“从原理到落地”的路线图。读完以后,你至少应该能回答下面几个问题:
如果你之前只是用过大模型聊天,这篇文章会帮你把“会说话”升级成“会办事”的完整思路。
简要介绍:Agent 不是一个单独的模型,而是一套围绕目标运行的系统。模型负责推理和决策,工具负责执行外部动作,记忆负责保存状态,上下文引擎负责筛选信息,调度层负责把这些组件串起来。真正有价值的 Agent,不是回答更长,而是闭环更完整。
先看一个最直观的对比。
| 维度 | 聊天机器人 | Agent 助手 |
|---|---|---|
| 目标 | 回答问题 | 完成任务 |
| 交互方式 | 一问一答 | 多轮循环 |
| 外部能力 | 通常没有 | 文件、搜索、代码、接口、数据库 |
| 状态管理 | 主要依赖当前上下文 | 有会话、记忆、任务进度 |
| 输出形式 | 文字回复 | 文字 + 动作 + 结果 |
| 失败处理 | 往往直接失败 | 可以重试、改计划、换工具 |
这个差异看起来简单,但工程意义非常大。聊天机器人更像“语言接口”,Agent 更像“任务执行系统”。前者适合解释、问答、生成文本;后者适合查询资料、整理文件、跑测试、执行工作流、自动化处理重复任务。
很多人第一次做 Agent 时,会习惯性地把所有需求都塞进一个超级长的 prompt 里,希望模型“自己想明白”。这通常会失败。原因很简单:模型虽然能推理,但它并不天然知道你手头有哪些工具、能访问哪些数据、当前任务已经做到哪一步、哪些结果已经验证过。Agent 的价值,就是把这些“原本缺失的工程能力”补齐。
从系统设计的角度看,Agent 有三个关键词:
很多初学者会问:既然大模型已经很强了,为什么还要自己实现 Agent?直接写一个很强的 prompt 不行吗?
短答案是不够。长答案是:prompt 只能影响模型如何回答,不能替代系统如何做事。
普通 prompt 的局限主要体现在下面几个方面:
所以,Agent 和 prompt 的关系不是替代,而是分工。prompt 负责给模型设定角色、约束和输出格式;Agent 负责把这个输出接到现实世界里,并让系统真的往前走。
一个很实用的判断标准是:
“如果任务只需要回答,不需要动作,不需要状态,不需要验证,那它更适合 prompt。”
“如果任务需要多步推进、需要调用工具、需要持续记忆、需要校验结果,那它更适合 Agent。”
并不是所有场景都适合上 Agent。真正适合 Agent 的任务,通常都满足下面几个特征:
典型场景包括:
但也要看到 Agent 的边界。它并不适合毫无约束地自主决策,更不适合没有审计、没有授权、没有人工确认的高风险操作。尤其是涉及删除文件、发起转账、修改生产环境配置、批量写入数据这类动作时,必须加上权限、审批和回滚机制。
最重要的一点是:Agent 不是“越自主越好”,而是“在可控范围内尽量自动化”。真正成熟的 Agent 不是放飞模型,而是给模型装上方向盘、刹车和仪表盘。
如果只用一句话解释 Agent 的工作方式,那就是:它不是一次性回答,而是一个循环。
这个循环可以概括为:
这个思想和学术界常说的 ReAct 很接近。它的关键不是“模型要不要思考”,而是“思考和行动要交替进行”。原因非常现实:很多任务不是靠模型记忆就能完成的,必须借助外部信息。比如你要知道某个文件内容,模型自己并不知道;你要确认某个接口返回什么,模型也必须调用接口之后才能知道。
从工程上看,这个循环最重要的价值有三个:
注意,这里的“思考”不一定意味着把完整推理过程暴露给用户。实际工程里,通常只需要模型输出结构化计划、动作和简短说明,而不是长篇自由发挥。换句话说,Agent 需要的是“可执行的思路”,不是“漂亮的废话”。
可以把这个循环画成下面这样:
flowchart LR U[用户目标] --> P[规划/决策] P --> A[调用工具] A --> O[观察结果] O --> R[反思与修正] R --> P R --> F[最终答案]
如果你把这个循环做稳了,Agent 就从“会聊天”变成了“会干活”。
很多人以为 Agent 的难点只是“会调用工具”。实际上,真正决定它能不能长期工作的是三件事:规划、记忆、上下文。
规划引擎负责把一个大目标拆成多个小目标。比如“帮我写一篇关于某项目的技术总结”,它不应该直接冲上去输出成稿,而是先拆成几个步骤:
如果没有规划,Agent 很容易一步到位地“脑补”答案,结果看起来像是完成了,实际上却没有验证。
记忆不是把所有历史原样存起来,而是区分不同层次:
短期记忆解决“这轮在说什么”;长期记忆解决“这个用户是谁、这个项目有哪些固定背景”;工作记忆解决“现在做到哪一步了”。
如果没有记忆,Agent 每一轮都会像失忆一样重新开始,复杂任务就会断掉。
上下文引擎负责“把该给模型看的内容挑出来”。这一步特别关键,因为大模型的上下文窗口虽然越来越长,但仍然不是无限的,而且“全塞进去”也不等于“更聪明”。相反,噪声太多时,模型更容易迷失。
一个好的上下文引擎通常会做四件事:
如果是代码类 Agent,context engine 往往还会结合 RepoMap、AST、文件摘要、测试结果和依赖关系图。这样做的好处是,不需要把整个仓库硬塞进去,也能让模型理解结构。
一个简单但非常实用的经验法则是:
“不是把所有信息都给模型,而是把最相关的信息以最干净的形式给模型。”
你可以把这个过程粗略理解为:
flowchart LR A[用户目标] --> B[检索候选信息] B --> C[排序] C --> D[压缩] D --> E[组装上下文] E --> F[交给模型]
这三块地基一旦打牢,Agent 的稳定性会比只靠 prompt 高很多。
工具调用是 Agent 从聊天机器人进化成执行系统的关键。所谓工具,可以是一个函数、一个命令、一个 HTTP 接口、一个数据库查询、一个搜索服务,也可以是浏览器、代码解释器、文件系统、Git、测试框架等能力。
这里有一个容易误解的地方:大模型通常并不是“自己执行工具”。更准确地说,模型会判断“我现在需要调用哪个工具,并给出调用参数”,真正的执行发生在你的应用程序里。应用程序执行完之后,再把结果返回给模型,模型根据结果继续回答或继续调用工具。
这个流程可以表示为:
sequenceDiagram participant U as 用户 participant A as Agent Runtime participant M as 大模型 participant T as 工具系统 U->>A: 提出目标 A->>M: 发送目标、上下文、可用工具 M->>A: 返回工具调用请求 A->>T: 校验权限并执行工具 T->>A: 返回执行结果 A->>M: 把工具结果交给模型 M->>A: 继续调用工具或生成最终答案 A->>U: 输出结果
工具调用最重要的不是“能调用”,而是“能安全、准确、可追踪地调用”。一个工程可用的工具系统至少要解决下面几个问题:
举个简单例子,如果用户说“帮我看看当前目录有哪些 Markdown 文件”,普通聊天机器人可能会凭空回答;Agent 则应该调用文件搜索工具,拿到真实结果之后再回答。这就是 Agent 可靠性的来源。
“反思”这个词听起来有点玄,但工程里它非常朴素。反思就是:执行一步之后,不要马上假设自己成功了,而是检查结果是否符合目标。
比如 Agent 执行了一个测试命令,返回失败。一个没有反思机制的系统可能直接把失败日志贴给用户;一个有反思机制的系统会继续判断:
这种能力不神秘,本质上就是把“结果检查”加入循环:
flowchart LR A[计划] --> B[执行] B --> C[观察] C --> D[判断是否达标] D -- 否 --> E[不达标就修正计划] E --> A D -- 是 --> F[继续/结束]
工程上可以把反思拆成三类:
反思机制越明确,Agent 越不容易陷入“看起来做了很多,实际上没完成”的状态。
按照一个偏工程化的 Agent 助手来设计,可以把系统分成“交互层、会话层、运行时、规划/记忆/上下文、工具路由、安全沙箱、工具系统、模型供应商”几大部分。下面这张图就是一个比较完整的架构蓝图:
flowchart TB A["CLI / TUI<br/>clap + ratatui + crossterm"] --> B["Session Layer<br/>restore / history"] B --> C["Agent Runtime<br/>think / plan / act / reflect loop"] C --> D["Planning Engine<br/>decomposition"] C --> E["Memory Engine<br/>STM / LTM"] D --> F["Context Engine<br/>RepoMap / AST / Compression / Ranking"] E --> F F --> G["Tool Router"] G --> H["Sandbox Layer"] G --> I["Provider Manager"] H --> H1["Process Sandbox"] H --> H2["Docker Sandbox"] H --> H3["Remote Sandbox"] H --> J["Tool System<br/>File / Shell / Git / Search / RepoMap / AST / Test"] I --> K["LLM Providers<br/>GPT / Claude / DeepSeek / Gemini / Ollama"]
这张图看起来模块很多,但不要被吓到。你可以先从最小版本做起:一个命令行入口、一个循环、两个工具、一个模型适配器、一个简单记忆文件,就已经能跑起来。后面所有复杂模块,本质上都是为了解决规模变大之后的稳定性、安全性和可维护性问题。
CLI 是命令行界面,TUI 是终端图形界面。对于初学者来说,CLI 最容易开始,因为它只需要输入文本、输出文本。等系统复杂以后,可以再做 TUI,把对话、计划、工具调用日志、文件变更、审批弹窗显示出来。
这一层的关键不是花哨,而是清楚:
如果用 Rust 做终端 Agent,clap 可以处理命令行参数,ratatui 可以绘制终端 UI,crossterm 可以处理键盘、屏幕和终端事件。如果用 Python 起步,也可以先用 argparse 和普通 print。
会话层负责保存一次任务的上下文,包括:
没有会话层,Agent 一旦中断就无法恢复。真正的开发助理经常需要处理长任务,比如“分析一个仓库并修复问题”。这个过程可能持续几十轮工具调用,如果中间断掉,必须能从历史状态恢复。
会话层常见的存储方式有三种:
初学者建议先用 JSONL,因为它最直观。
Agent Runtime 是整个系统的核心。它负责运行“think / plan / act / reflect”循环。这里的 runtime 不一定很复杂,但它必须掌控流程,而不能把控制权完全交给模型。
Runtime 至少要做这些事:
为什么要限制最大循环次数?因为模型可能会陷入循环,比如一直搜索、一直读文件、一直尝试相同命令。如果没有上限,Agent 会浪费大量 token 和时间,甚至造成危险操作。
一个实用设置是:普通任务最多 5 到 10 轮,代码任务最多 20 到 40 轮,长任务需要显式进入后台模式,并持续保存状态。
规划引擎负责把目标拆成步骤,并动态更新。规划不一定非要由单独模型完成,初期可以让主模型输出一个简短计划。随着任务复杂度上升,可以独立出 planner。
一个好的计划应该满足四个要求:
比如用户说:“帮我检查这个 Python 项目为什么测试失败。”一个合理计划是:
1. 查看项目结构和测试配置。
2. 运行测试命令复现失败。
3. 阅读失败日志定位文件和函数。
4. 查看相关源码和测试。
5. 修改最小范围代码。
6. 重新运行测试确认。
7. 总结原因和改动。
这个计划的好处是,每一步都有明确动作,不是泛泛地说“分析问题”。
记忆引擎不要变成垃圾桶。很多 Agent 项目失败,就是因为把所有历史都叫“记忆”,最后模型每次都收到一堆无关内容。
更好的做法是按用途分层:
| 类型 | 保存内容 | 生命周期 | 示例 |
|---|---|---|---|
| 短期记忆 | 最近对话和工具结果 | 当前任务 | 最近一次测试失败日志 |
| 工作记忆 | 当前计划和状态 | 当前任务 | 已读取 main.py,待检查 test_api.py |
| 长期记忆 | 稳定偏好和规则 | 多任务复用 | 用户偏好中文回答、代码注释用中文 |
| 项目记忆 | 仓库结构和约束 | 某个项目 | 使用 Poetry、测试命令为 pytest |
记忆还要支持“遗忘”。过期、错误、重复的信息应该被压缩或删除。否则长期运行后,Agent 会被旧信息污染。
上下文引擎是 Agent 工程里非常容易被低估的模块。模型输出质量很大程度上取决于输入质量,而输入质量不只是 prompt 写得好,还包括材料选得准。
以代码 Agent 为例,用户问“为什么登录接口测试失败”,上下文引擎不应该把整个仓库都给模型,而应该优先找:
这就需要结合搜索、AST、文件依赖、调用关系和历史工具结果。
一个简单的 Context Engine 可以先这样做:
flowchart LR A[关键词搜索] --> B[文件路径打分] B --> C[读取片段] C --> D[截断过长内容] D --> E[拼装 Prompt]
更复杂一点,可以加入:
注意,上下文压缩不是简单地“缩短文字”,而是保留对当前决策有用的信息。比如错误日志里最重要的是异常类型、栈顶文件、断言差异,而不是完整重复日志。
Tool Router 负责把模型请求的工具调用分发给真正的工具实现。它不仅是路由器,也是守门人。
它需要做的事包括:
比如模型请求执行:
{
"tool": "shell",
"args": {
"cmd": "rm -rf /"
}
}
一个合格的 Tool Router 必须拦截它,而不是天真地执行。Agent 的能力越强,Tool Router 越重要。
沙箱层负责限制工具的执行环境。没有沙箱的 Agent 就像一个拿着管理员权限的实习生,能干活,但风险极高。
常见沙箱分三类:
对于个人项目,最小沙箱策略可以是:
安全不是上线前才考虑的事。Agent 从第一天就应该有边界。
不同模型供应商的 API、工具调用格式、流式输出、上下文长度、价格、速度都不同。Provider Manager 的职责是把这些差异封装起来,让 Agent Runtime 只面对统一接口。
统一接口可以长这样:
输入:messages + tools + model_config
输出:final_answer 或 tool_call
这样你就可以根据任务选择模型:
一个成熟 Agent 不应该被单一模型绑定死。模型会变化,价格会变化,能力也会变化,架构上留出 provider 抽象是值得的。
工具系统是 Agent 真正接触世界的地方。常见工具包括:
工具越多,Agent 越强,但工具越多也越容易混乱。所以工具设计要遵循“少而清晰”的原则。每个工具都应该职责单一、参数明确、返回结果稳定。
下面用一个“让 Agent 帮我们分析项目测试失败”的例子,看看整个系统如何协作。
sequenceDiagram participant User as 用户 participant CLI as CLI/TUI participant Session as Session Layer participant Runtime as Agent Runtime participant Planner as Planning Engine participant Context as Context Engine participant Router as Tool Router participant Tool as Tool System participant LLM as LLM Provider User->>CLI: 帮我修复测试失败 CLI->>Session: 创建会话并记录请求 Session->>Runtime: 启动任务 Runtime->>Planner: 生成初始计划 Planner->>Runtime: 返回步骤列表 Runtime->>Context: 获取相关上下文 Context->>Runtime: 返回项目结构和历史摘要 Runtime->>LLM: 请求下一步动作 LLM->>Runtime: 建议运行测试 Runtime->>Router: 请求执行 test 工具 Router->>Tool: 执行 pytest Tool->>Router: 返回失败日志 Router->>Runtime: 返回观察结果 Runtime->>Session: 写入工具调用记录 Runtime->>LLM: 提交失败日志并请求分析 LLM->>Runtime: 建议读取相关源码 Runtime->>Router: 调用 file 工具 Router->>Tool: 读取文件片段 Tool->>Router: 返回源码 Router->>Runtime: 返回观察结果 Runtime->>LLM: 请求修复方案 LLM->>Runtime: 返回修改建议 Runtime->>Router: 调用 file 工具写入补丁 Router->>Tool: 修改文件 Tool->>Router: 返回修改结果 Runtime->>Router: 再次运行测试 Router->>Tool: 执行测试 Tool->>Router: 返回通过 Runtime->>Session: 保存最终状态 Runtime->>CLI: 输出原因、改动和验证结果
这个过程体现了 Agent 的本质:它不是一次回答,而是一组带状态的动作链。
下面我们用 Python 写一个最小 Agent。它不依赖真实大模型,而是用一个规则模型模拟“模型决策”,这样你可以直接理解架构逻辑。真实项目里,只需要把 RuleBasedProvider 换成真正的 LLM Provider 即可。
这个示例支持两个工具:
list_files:列出当前目录文件。read_file:读取指定文件。它的目标不是炫技,而是把 Agent 的核心循环讲清楚。
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
@dataclass
class ToolCall:
name: str
args: dict[str, Any]
@dataclass
class ModelResult:
final_answer: str | None = None
tool_call: ToolCall | None = None
@dataclass
class StepRecord:
role: str
content: str
@dataclass
class AgentState:
goal: str
steps: list[StepRecord] = field(default_factory=list)
def remember(self, role: str, content: str) -> None:
# 简单记录当前任务轨迹,方便后续恢复和调试。
self.steps.append(StepRecord(role=role, content=content))
class ToolRegistry:
def __init__(self) -> None:
self._tools: dict[str, Callable[..., str]] = {}
def register(self, name: str, func: Callable[..., str]) -> None:
# 工具名要稳定,模型后续会通过名字请求调用。
self._tools[name] = func
def call(self, tool_call: ToolCall) -> str:
if tool_call.name not in self._tools:
raise ValueError(f"未知工具:{tool_call.name}")
# 真正项目里这里要做参数校验、权限检查和审计日志。
return self._tools[tool_call.name](**tool_call.args)
class FileTools:
def __init__(self, root: Path) -> None:
self.root = root.resolve()
def _safe_path(self, relative_path: str) -> Path:
target = (self.root / relative_path).resolve()
# 防止模型通过 ../ 读取工作目录外的文件。
if not str(target).startswith(str(self.root)):
raise PermissionError("禁止访问工作目录之外的路径")
return target
def list_files(self) -> str:
files = []
for path in sorted(self.root.iterdir()):
if path.is_file():
files.append(path.name)
return "\n".join(files) if files else "当前目录没有文件"
def read_file(self, path: str) -> str:
target = self._safe_path(path)
if not target.exists():
return f"文件不存在:{path}"
if not target.is_file():
return f"不是普通文件:{path}"
# 限制读取长度,避免大文件撑爆上下文。
return target.read_text(encoding="utf-8", errors="replace")[:4000]
class RuleBasedProvider:
"""用规则模拟模型决策,便于初学者理解 Agent 循环。"""
def next_action(self, state: AgentState) -> ModelResult:
history = "\n".join(step.content for step in state.steps)
if "列出文件结果" not in history:
return ModelResult(tool_call=ToolCall(name="list_files", args={}))
if "build-your-own-agent-assistant.md" in history and "读取文件结果" not in history:
return ModelResult(
tool_call=ToolCall(
name="read_file",
args={"path": "build-your-own-agent-assistant.md"},
)
)
return ModelResult(
final_answer=(
"我已经查看了当前目录,并读取了目标 Markdown 文件。"
"这个最小 Agent 完成了:理解目标、选择工具、执行工具、记录结果、生成最终回答。"
)
)
class AgentRuntime:
def __init__(self, provider: RuleBasedProvider, tools: ToolRegistry, max_rounds: int = 5) -> None:
self.provider = provider
self.tools = tools
self.max_rounds = max_rounds
def run(self, goal: str) -> str:
state = AgentState(goal=goal)
state.remember("user", goal)
for round_index in range(1, self.max_rounds + 1):
# 每一轮都让模型根据当前状态决定下一步。
result = self.provider.next_action(state)
if result.final_answer:
state.remember("assistant", result.final_answer)
return result.final_answer
if not result.tool_call:
return "模型没有给出最终答案,也没有请求工具调用。"
state.remember(
"assistant",
f"第 {round_index} 轮请求工具:{result.tool_call.name} {result.tool_call.args}",
)
try:
observation = self.tools.call(result.tool_call)
except Exception as exc:
observation = f"工具执行失败:{exc}"
# 工具结果要写回状态,下一轮模型才能基于真实观察继续决策。
if result.tool_call.name == "list_files":
state.remember("tool", f"列出文件结果:\n{observation}")
else:
state.remember("tool", f"读取文件结果:\n{observation[:500]}")
return "达到最大循环次数,任务被中止。"
def main() -> None:
root = Path(".")
file_tools = FileTools(root)
registry = ToolRegistry()
registry.register("list_files", file_tools.list_files)
registry.register("read_file", file_tools.read_file)
agent = AgentRuntime(provider=RuleBasedProvider(), tools=registry)
answer = agent.run("请检查当前目录里的博客文件")
print(answer)
if __name__ == "__main__":
main()
这段代码虽小,但已经包含了 Agent 的核心结构:
AgentRuntime 负责循环。Provider 负责决策。ToolRegistry 负责工具路由。FileTools 负责真实执行。AgentState 负责保存任务状态。max_rounds 负责防止无限循环。你可以把它看作 Agent 的“骨架”。后面所有复杂能力,都是在这个骨架上加模块。
接入真实大模型之后,核心变化是:RuleBasedProvider.next_action() 不再用 if 判断,而是调用模型 API,让模型返回结构化结果。结构化结果通常有两类:
为了让模型输出稳定,建议让它返回类似下面的 JSON:
{
"type": "tool_call",
"tool": "read_file",
"args": {
"path": "README.md"
},
"reason": "需要读取项目说明来理解启动方式"
}
或者:
{
"type": "final",
"answer": "我已经完成检查,问题原因是测试配置缺少环境变量。"
}
模型输出越结构化,Runtime 越容易控制。不要让模型自由输出“我觉得应该调用 read_file 工具,参数大概是 README.md”,因为这种文本很难稳定解析。
一个 Provider 抽象可以这样设计:
from dataclasses import dataclass
from typing import Any, Protocol
@dataclass
class ProviderInput:
system_prompt: str
messages: list[dict[str, str]]
tools: list[dict[str, Any]]
@dataclass
class ProviderOutput:
kind: str
content: str | None = None
tool_name: str | None = None
tool_args: dict[str, Any] | None = None
class LLMProvider(Protocol):
def generate(self, payload: ProviderInput) -> ProviderOutput:
"""统一模型接口,隐藏不同供应商的 API 差异。"""
...
这样做的好处是,Agent Runtime 不关心底层用的是 GPT、Claude、DeepSeek、Gemini 还是 Ollama。只要 provider 返回统一的 ProviderOutput,上层逻辑就能继续工作。
系统提示词也要尽量工程化。例如:
你是一个本地 Agent 助手,目标是帮助用户完成任务。
规则:
1. 如果需要外部信息,必须通过工具获取,不要猜测。
2. 每次只能调用一个工具,除非系统明确允许并行调用。
3. 高风险操作必须说明原因,并等待用户确认。
4. 如果工具返回失败,要根据失败原因调整计划。
5. 当信息足够时,输出最终答案。
输出格式:
如果需要工具,返回 JSON:
{"type":"tool_call","tool":"工具名","args":{},"reason":"原因"}
如果任务完成,返回 JSON:
{"type":"final","answer":"最终答案"}
这里的重点不是 prompt 多长,而是边界清楚、格式稳定、责任明确。
文件工具相对安全,Shell 工具就危险得多。因为 Shell 能做的事情太多:删除文件、访问网络、读取密钥、启动进程、安装依赖、修改系统配置。给 Agent 接 Shell 工具之前,必须先加安全限制。
下面是一个简化版 Shell 工具,只允许执行白名单命令,并且设置超时。
import shlex
import subprocess
from dataclasses import dataclass
@dataclass
class ShellResult:
code: int
stdout: str
stderr: str
class SafeShellTool:
def __init__(self, allowed_commands: set[str], timeout_seconds: int = 10) -> None:
self.allowed_commands = allowed_commands
self.timeout_seconds = timeout_seconds
def run(self, command: str) -> ShellResult:
parts = shlex.split(command)
if not parts:
return ShellResult(code=1, stdout="", stderr="空命令")
executable = parts[0]
if executable not in self.allowed_commands:
# 默认拒绝未知命令,避免模型执行危险操作。
return ShellResult(code=126, stdout="", stderr=f"命令未授权:{executable}")
try:
completed = subprocess.run(
parts,
text=True,
capture_output=True,
timeout=self.timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired:
return ShellResult(code=124, stdout="", stderr="命令执行超时")
# 裁剪输出长度,避免日志过大影响后续上下文。
return ShellResult(
code=completed.returncode,
stdout=completed.stdout[:4000],
stderr=completed.stderr[:4000],
)
if __name__ == "__main__":
shell = SafeShellTool(allowed_commands={"python", "pytest", "ls", "pwd"})
result = shell.run("pwd")
print(result)
真实项目里还应该继续增强:
不要因为 Agent “看起来聪明”就放松安全。越聪明的 Agent,越需要明确的权限边界。
最小记忆系统可以用 JSONL 文件实现。JSONL 的特点是一行一个 JSON,追加写入很方便,也方便调试。
import json
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
@dataclass
class MemoryItem:
time: str
scope: str
content: str
class JsonlMemory:
def __init__(self, path: Path) -> None:
self.path = path
self.path.parent.mkdir(parents=True, exist_ok=True)
def add(self, scope: str, content: str) -> None:
item = MemoryItem(
time=datetime.now().isoformat(timespec="seconds"),
scope=scope,
content=content,
)
with self.path.open("a", encoding="utf-8") as file:
# 每条记忆独立成行,方便后续增量读取。
file.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")
def search(self, keyword: str, limit: int = 5) -> list[MemoryItem]:
if not self.path.exists():
return []
matches: list[MemoryItem] = []
for line in self.path.read_text(encoding="utf-8").splitlines():
raw = json.loads(line)
item = MemoryItem(**raw)
if keyword.lower() in item.content.lower():
matches.append(item)
# 返回最近的匹配项,避免旧信息污染上下文。
return matches[-limit:]
if __name__ == "__main__":
memory = JsonlMemory(Path(".agent/memory.jsonl"))
memory.add("preference", "用户偏好:默认使用中文解释,代码示例添加简短中文注释。")
print(memory.search("中文"))
这个实现非常简单,但已经具备三个关键点:
后续可以升级为 SQLite、向量数据库、全文索引,甚至把记忆分成用户级、项目级、任务级。但在早期,不要过度设计。先把“记什么、什么时候记、怎么用”想清楚,比一上来接复杂数据库更重要。
当工具返回大量内容时,不能直接全部塞给模型。比如一个测试日志可能有几万行,一个代码仓库可能有几千个文件。如果不压缩,上下文会很快爆掉,模型也会抓不到重点。
一个最小的压缩策略可以是:
def compress_text(text: str, max_chars: int = 3000) -> str:
if len(text) <= max_chars:
return text
head = text[: max_chars // 2]
tail = text[-max_chars // 2 :]
# 保留开头和结尾,中间用提示说明省略。
return f"{head}\n\n...中间内容已压缩,保留首尾关键信息...\n\n{tail}"
但对不同内容,应该使用不同压缩策略:
也就是说,压缩不是机械截断,而是围绕当前目标保留决策所需信息。
如果你准备把 Agent 做成长期项目,可以从下面这个目录结构开始:
my-agent/
├── agent/
│ ├── __init__.py
│ ├── runtime.py
│ ├── state.py
│ ├── planner.py
│ ├── memory.py
│ ├── context.py
│ ├── prompts.py
│ └── errors.py
├── providers/
│ ├── __init__.py
│ ├── base.py
│ ├── openai_provider.py
│ ├── claude_provider.py
│ └── ollama_provider.py
├── tools/
│ ├── __init__.py
│ ├── base.py
│ ├── file_tool.py
│ ├── shell_tool.py
│ ├── git_tool.py
│ ├── search_tool.py
│ └── test_tool.py
├── sandbox/
│ ├── __init__.py
│ ├── policy.py
│ └── process_sandbox.py
├── sessions/
│ └── .gitkeep
├── tests/
│ ├── test_runtime.py
│ ├── test_tools.py
│ └── test_memory.py
├── cli.py
└── pyproject.toml
这个结构的原则是“运行时和工具分离、模型和业务分离、安全策略单独管理”。这样后面扩展时不会把所有代码塞进一个巨大文件。
最小 Agent 能跑起来,不代表能稳定上线。生产化至少要补齐下面这些能力:
很多 Agent Demo 看起来很酷,但一到真实环境就容易翻车,原因往往不是模型不够强,而是这些工程能力缺失。
很多初学者会把这三个概念混在一起,其实它们解决的问题不一样。
Function Calling 解决的是“模型如何请求一次具体动作”。模型看到任务后,决定要调用哪个函数、传什么参数,应用程序再把这个函数真正执行掉。它更像“单次动作接口”。
适合的场景包括:
Agents SDK 解决的是“谁来编排整个任务循环”。官方文档把 Agent 描述为:会规划、会调用工具、能协作、并且能保留足够状态来完成多步工作的应用。换句话说,SDK 更关注运行时、状态、审批、工具执行和多步工作流。
如果你的应用需要自己掌控:
那就更适合引入这类运行时框架,而不是只做一次函数调用。
MCP 解决的是“如何用统一协议连接外部系统”。它更像一层标准化连接方式,负责把数据源、工具、工作流暴露给 AI 应用,让不同客户端和不同服务器之间能以统一方式协作。
你可以把三者理解成一条链:
一个简化的关系图如下:
flowchart LR User[用户目标] --> Runtime[Agent Runtime] Runtime --> Model[大模型] Model -->|请求函数| FC[Function Calling] Runtime -->|编排任务| SDK[Agents SDK / 自研 Runtime] Runtime -->|连接外部系统| MCP[MCP Server] MCP --> Tools[文件/数据库/搜索/工作流]
对于个人项目来说,最实用的路线通常是:
这样做的好处是,学习曲线更平滑,系统也更容易演进。
Agent 助手的本质,不是“给聊天机器人加几个工具”,而是构建一个围绕目标持续运行的任务系统。它需要模型的推理能力,也需要传统软件工程里的状态管理、权限控制、错误处理、日志追踪和模块化设计。
笔者从概念、原理、架构到代码示例,完整梳理了一个 Agent 助手的实现路径。你可以从最小版本开始:一个 Runtime、一个 Provider、一个 ToolRegistry、几个安全工具、一个 JSONL 记忆文件。等它跑稳之后,再逐步加入规划引擎、上下文引擎、沙箱、模型供应商管理、多 Agent 协作和评测系统。
对初学者来说,最重要的不是一开始就做一个“全自动超级智能体”,而是先理解闭环:
flowchart LR A[目标] --> B[计划] B --> C[工具调用] C --> D[观察结果] D --> E[反思修正] E --> F[最终交付]
只要这个闭环跑通,你就已经跨过了从“会聊天”到“会办事”的门槛。
未来 Agent 的前景非常明确。它会从单纯的问答入口,逐步变成个人工作台、开发环境、企业流程系统和自动化基础设施的一部分。它不会替代所有软件,但会重塑很多软件的交互方式:用户不再只点击按钮,而是直接描述目标;系统不再只展示信息,而是主动规划和执行。
不过,越是强大的 Agent,越需要可控。真正值得信任的 Agent,不是永远自信地往前冲,而是在需要信息时会查证,在需要操作时会申请权限,在失败时会修正路线,在完成后能给出清晰证据。
如果要用一句话收尾,那就是:
Agent 的未来不是“模型自己统治一切”,而是“模型成为软件系统中会推理、会协作、会调用工具的核心执行单元”。
这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!
另外,博主出新书了《Hadoop与Spark大数据全景解析》、同时已出版的《深入理解Hive》、《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》也可以和新书配套使用,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。