Agent 架构设计与能力构建

1.概述

很多人第一次听到"Agent"这个词时,最容易把它理解成"更聪明的聊天机器人"。这个理解只对了一半。Agent 确实建立在大模型之上,但它的重点不是"会说",而是"会做"。它不是只回答你一句话,而是能围绕一个目标持续推进:拆解任务、调用工具、观察结果、修正计划、保存状态,最后把事情做完。

如果把普通聊天模型比作"会表达的脑子",那 Agent 更像"带着记忆和手脚的执行系统"。它不只回答"这是什么",还会处理"去完成这件事"。这也是为什么越来越多的开发者开始自己实现 Agent 助手,而不是只停留在一个聊天框里。

本文将从工程实践视角出发,深入解析 Agent 助手的核心原理与系统架构,帮助读者理解一个 Agent 如何完成感知、决策、记忆与执行。通过架构拆解、模块分析以及代码示例,展示如何以最小成本构建一个可扩展、可演进的 Agent 原型,并为后续的复杂场景落地提供参考。

你可以把本文理解成一份"从原理到落地"的路线图。读完以后,你至少应该能回答下面几个问题:

  1. Agent 和普通聊天机器人到底差在哪。
  2. 为什么单靠 prompt 不足以支撑一个真正能干活的助手。
  3. 一个实用的 Agent 系统通常由哪些模块组成。
  4. 怎么把"思考 - 行动 - 观察 - 反思"做成一个稳定的循环。
  5. 如何把工具、记忆、上下文和安全控制接进来。
  6. 未来 Agent 会往什么方向演进。

如果你之前只是用过大模型聊天,这篇文章会帮你把"会说话"升级成"会办事"的完整思路。

2.内容

简要介绍:Agent 不是一个单独的模型,而是一套围绕目标运行的系统。模型负责推理和决策,工具负责执行外部动作,记忆负责保存状态,上下文引擎负责筛选信息,调度层负责把这些组件串起来。真正有价值的 Agent,不是回答更长,而是闭环更完整。

2.1 Agent 到底是什么,它和聊天机器人有什么区别

先看一个最直观的对比。

维度 聊天机器人 Agent 助手
目标 回答问题 完成任务
交互方式 一问一答 多轮循环
外部能力 通常没有 文件、搜索、代码、接口、数据库
状态管理 主要依赖当前上下文 有会话、记忆、任务进度
输出形式 文字回复 文字 + 动作 + 结果
失败处理 往往直接失败 可以重试、改计划、换工具

这个差异看起来简单,但工程意义非常大。聊天机器人更像"语言接口",Agent 更像"任务执行系统"。前者适合解释、问答、生成文本;后者适合查询资料、整理文件、跑测试、执行工作流、自动化处理重复任务。

很多人第一次做 Agent 时,会习惯性地把所有需求都塞进一个超级长的 prompt 里,希望模型"自己想明白"。这通常会失败。原因很简单:模型虽然能推理,但它并不天然知道你手头有哪些工具、能访问哪些数据、当前任务已经做到哪一步、哪些结果已经验证过。Agent 的价值,就是把这些"原本缺失的工程能力"补齐。

从系统设计的角度看,Agent 有三个关键词:

  1. 目标导向。它不是闲聊,而是围绕一个明确目标推进。
  2. 闭环控制。它不是一次性输出,而是"决策 - 执行 - 观察 - 再决策"的循环。
  3. 外部扩展。它不是只靠模型内部知识,而是可以接工具、接文件、接数据库、接搜索、接自动化环境。

2.2 为什么普通 prompt 不够

很多初学者会问:既然大模型已经很强了,为什么还要自己实现 Agent?直接写一个很强的 prompt 不行吗?

短答案是不够。长答案是:prompt 只能影响模型如何回答,不能替代系统如何做事。

普通 prompt 的局限主要体现在下面几个方面:

  1. 上下文窗口是有限的。你不能把整个项目、整个知识库、所有历史对话都塞进去。
  2. 模型不自带外部执行能力。它可以告诉你"应该怎么做",但不能自动去读文件、查数据库、跑命令。
  3. 模型会出错,而且错得很自信。没有反馈回路时,错误很难及时纠正。
  4. 长任务需要状态。比如"整理一周报告"不是一句话就能结束,它需要持续追踪中间结果。
  5. 复杂任务需要拆解。比如"帮我分析这个仓库问题并给出修复建议",通常要经历定位、阅读、验证、测试、总结多个步骤。

所以,Agent 和 prompt 的关系不是替代,而是分工。prompt 负责给模型设定角色、约束和输出格式;Agent 负责把这个输出接到现实世界里,并让系统真的往前走。

一个很实用的判断标准是:

"如果任务只需要回答,不需要动作,不需要状态,不需要验证,那它更适合 prompt。"

"如果任务需要多步推进、需要调用工具、需要持续记忆、需要校验结果,那它更适合 Agent。"

2.3 Agent 适合哪些场景

并不是所有场景都适合上 Agent。真正适合 Agent 的任务,通常都满足下面几个特征:

  1. 目标明确。
  2. 步骤可拆解。
  3. 可以借助外部工具。
  4. 结果可以验证。
  5. 中间状态值得保存。

典型场景包括:

  1. 个人助理:帮你整理日程、查询资料、总结邮件、归纳会议纪要。
  2. 开发助理:帮你读代码、找问题、跑测试、改配置、生成补丁。
  3. 知识助理:连接文档库、Wiki、数据库、内部资料,支持问答和检索。
  4. 运维助理:检查日志、分析告警、执行常见排障步骤。
  5. 研究助理:自动搜索资料、提炼结论、归纳对比、输出报告。
  6. 流程助理:把重复但有规则的业务流程自动化,比如工单处理、内容审核、表单整理。

但也要看到 Agent 的边界。它并不适合毫无约束地自主决策,更不适合没有审计、没有授权、没有人工确认的高风险操作。尤其是涉及删除文件、发起转账、修改生产环境配置、批量写入数据这类动作时,必须加上权限、审批和回滚机制。

最重要的一点是:Agent 不是"越自主越好",而是"在可控范围内尽量自动化"。真正成熟的 Agent 不是放飞模型,而是给模型装上方向盘、刹车和仪表盘。

3.原理

3.1 Agent 的核心是"思考 - 行动 - 观察"循环

如果只用一句话解释 Agent 的工作方式,那就是:它不是一次性回答,而是一个循环。

这个循环可以概括为:

  1. 先理解目标。
  2. 再制定下一步动作。
  3. 调用工具去执行动作。
  4. 读取工具返回的结果。
  5. 根据结果调整计划。
  6. 如果已经足够,就输出最终答案。

这个思想和学术界常说的 ReAct 很接近。它的关键不是"模型要不要思考",而是"思考和行动要交替进行"。原因非常现实:很多任务不是靠模型记忆就能完成的,必须借助外部信息。比如你要知道某个文件内容,模型自己并不知道;你要确认某个接口返回什么,模型也必须调用接口之后才能知道。

从工程上看,这个循环最重要的价值有三个:

  1. 降低幻觉。工具给出的结果比模型凭空猜测更可靠。
  2. 支持多步任务。每一步都建立在前一步的结果上。
  3. 便于调试。你可以记录每一次动作和观察,知道问题出在哪一轮。

注意,这里的"思考"不一定意味着把完整推理过程暴露给用户。实际工程里,通常只需要模型输出结构化计划、动作和简短说明,而不是长篇自由发挥。换句话说,Agent 需要的是"可执行的思路",不是"漂亮的废话"。

可以把这个循环画成下面这样:
flowchart LR U用户目标 --> P规划/决策 P --> A调用工具 A --> O观察结果 O --> R反思与修正 R --> P R --> F最终答案

如果你把这个循环做稳了,Agent 就从"会聊天"变成了"会干活"。

3.2 规划、记忆与上下文压缩为什么是 Agent 的三块地基

很多人以为 Agent 的难点只是"会调用工具"。实际上,真正决定它能不能长期工作的是三件事:规划、记忆、上下文。

规划引擎

规划引擎负责把一个大目标拆成多个小目标。比如"帮我写一篇关于某项目的技术总结",它不应该直接冲上去输出成稿,而是先拆成几个步骤:

  1. 找到项目目录结构。
  2. 阅读核心文件。
  3. 提取关键功能和设计点。
  4. 梳理问题和风险。
  5. 组织成文章结构。
  6. 再生成正文。

如果没有规划,Agent 很容易一步到位地"脑补"答案,结果看起来像是完成了,实际上却没有验证。

记忆引擎

记忆不是把所有历史原样存起来,而是区分不同层次:

  1. 短期记忆:当前任务的最近几轮对话、最近几次工具结果。
  2. 长期记忆:用户偏好、稳定事实、任务摘要、项目约束。
  3. 工作记忆:当前正在处理的中间状态,比如"已完成搜索""待验证测试""待写报告"。

短期记忆解决"这轮在说什么";长期记忆解决"这个用户是谁、这个项目有哪些固定背景";工作记忆解决"现在做到哪一步了"。

如果没有记忆,Agent 每一轮都会像失忆一样重新开始,复杂任务就会断掉。

上下文引擎

上下文引擎负责"把该给模型看的内容挑出来"。这一步特别关键,因为大模型的上下文窗口虽然越来越长,但仍然不是无限的,而且"全塞进去"也不等于"更聪明"。相反,噪声太多时,模型更容易迷失。

一个好的上下文引擎通常会做四件事:

  1. 检索。先找出和当前任务最相关的片段。
  2. 排序。再按相关性、时效性、重要性重新排列。
  3. 压缩。把长文、历史对话、代码段压成更短的摘要。
  4. 组装。最后把最有用的材料送进模型上下文。

如果是代码类 Agent,context engine 往往还会结合 RepoMap、AST、文件摘要、测试结果和依赖关系图。这样做的好处是,不需要把整个仓库硬塞进去,也能让模型理解结构。

一个简单但非常实用的经验法则是:

"不是把所有信息都给模型,而是把最相关的信息以最干净的形式给模型。"

你可以把这个过程粗略理解为:
flowchart LR A用户目标 --> B检索候选信息 B --> C排序 C --> D压缩 D --> E组装上下文 E --> F交给模型

这三块地基一旦打牢,Agent 的稳定性会比只靠 prompt 高很多。

3.3 工具调用:让模型从"建议者"变成"操作者"

工具调用是 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: 输出结果

工具调用最重要的不是"能调用",而是"能安全、准确、可追踪地调用"。一个工程可用的工具系统至少要解决下面几个问题:

  1. 工具描述要清楚。模型需要知道工具做什么、参数是什么、什么时候应该用。
  2. 参数要结构化。尽量使用 JSON Schema 或明确的数据结构,减少自由文本歧义。
  3. 执行要可控。危险操作必须经过权限检查、沙箱隔离或人工确认。
  4. 结果要可读。工具返回值不能是一堆难以理解的原始日志,需要经过裁剪和格式化。
  5. 调用要可审计。每次调用的工具名、参数、耗时、结果、错误都应该记录。

举个简单例子,如果用户说"帮我看看当前目录有哪些 Markdown 文件",普通聊天机器人可能会凭空回答;Agent 则应该调用文件搜索工具,拿到真实结果之后再回答。这就是 Agent 可靠性的来源。

3.4 反思机制:不是让模型自言自语,而是让系统及时纠偏

"反思"这个词听起来有点玄,但工程里它非常朴素。反思就是:执行一步之后,不要马上假设自己成功了,而是检查结果是否符合目标。

比如 Agent 执行了一个测试命令,返回失败。一个没有反思机制的系统可能直接把失败日志贴给用户;一个有反思机制的系统会继续判断:

  1. 失败是环境问题还是代码问题?
  2. 是否缺少依赖?
  3. 是否是测试命令选错了?
  4. 是否应该读取失败文件进一步定位?
  5. 是否需要换一个更小范围的测试?

这种能力不神秘,本质上就是把"结果检查"加入循环:
flowchart LR A计划 --> B执行 B --> C观察 C --> D判断是否达标 D -- 否 --> E不达标就修正计划 E --> A D -- 是 --> F继续/结束

工程上可以把反思拆成三类:

  1. 格式反思。输出是否符合格式,比如是否是合法 JSON。
  2. 结果反思。工具执行结果是否符合预期,比如测试是否通过。
  3. 目标反思。当前进度是否真的接近用户目标,而不是跑偏。

反思机制越明确,Agent 越不容易陷入"看起来做了很多,实际上没完成"的状态。

4.架构设计与实战落地

4.1 总体架构图

按照一个偏工程化的 Agent 助手来设计,可以把系统分成"交互层、会话层、运行时、规划/记忆/上下文、工具路由、安全沙箱、工具系统、模型供应商"几大部分。下面这张图就是一个比较完整的架构蓝图:
flowchart TB A"CLI / TUI\
clap + ratatui + crossterm"
--> B"Session Layer\
restore / history"
B --> C"Agent Runtime\
think / plan / act / reflect loop"
C --> D"Planning Engine\
decomposition"
C --> E"Memory Engine\
STM / LTM"
D --> F"Context Engine\
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\
File / Shell / Git / Search / RepoMap / AST / Test"
I --> K"LLM Providers\
GPT / Claude / DeepSeek / Gemini / Ollama"

这张图看起来模块很多,但不要被吓到。你可以先从最小版本做起:一个命令行入口、一个循环、两个工具、一个模型适配器、一个简单记忆文件,就已经能跑起来。后面所有复杂模块,本质上都是为了解决规模变大之后的稳定性、安全性和可维护性问题。

4.2 每一层到底负责什么

CLI / TUI:用户和 Agent 的入口

CLI 是命令行界面,TUI 是终端图形界面。对于初学者来说,CLI 最容易开始,因为它只需要输入文本、输出文本。等系统复杂以后,可以再做 TUI,把对话、计划、工具调用日志、文件变更、审批弹窗显示出来。

这一层的关键不是花哨,而是清楚:

  1. 用户输入目标。
  2. 系统显示当前计划。
  3. 系统展示正在调用的工具。
  4. 高风险动作需要用户确认。
  5. 最终结果要能复制、保存、复现。

如果用 Rust 做终端 Agent,clap 可以处理命令行参数,ratatui 可以绘制终端 UI,crossterm 可以处理键盘、屏幕和终端事件。如果用 Python 起步,也可以先用 argparse 和普通 print

Session Layer:让 Agent 不失忆

会话层负责保存一次任务的上下文,包括:

  1. 用户原始请求。
  2. 每一轮模型输出。
  3. 每一次工具调用。
  4. 工具返回结果。
  5. 当前任务状态。
  6. 最终答案。

没有会话层,Agent 一旦中断就无法恢复。真正的开发助理经常需要处理长任务,比如"分析一个仓库并修复问题"。这个过程可能持续几十轮工具调用,如果中间断掉,必须能从历史状态恢复。

会话层常见的存储方式有三种:

  1. JSONL 文件。简单、可读、容易调试。
  2. SQLite。适合本地 Agent,支持查询和索引。
  3. 服务端数据库。适合团队协作和多设备同步。

初学者建议先用 JSONL,因为它最直观。

Agent Runtime:系统的大脑和调度器

Agent Runtime 是整个系统的核心。它负责运行"think / plan / act / reflect"循环。这里的 runtime 不一定很复杂,但它必须掌控流程,而不能把控制权完全交给模型。

Runtime 至少要做这些事:

  1. 构造模型输入。
  2. 调用模型。
  3. 解析模型输出。
  4. 判断是最终回答还是工具调用。
  5. 执行工具调用。
  6. 把工具结果写回上下文。
  7. 控制最大循环次数。
  8. 处理错误、超时和重试。

为什么要限制最大循环次数?因为模型可能会陷入循环,比如一直搜索、一直读文件、一直尝试相同命令。如果没有上限,Agent 会浪费大量 token 和时间,甚至造成危险操作。

一个实用设置是:普通任务最多 5 到 10 轮,代码任务最多 20 到 40 轮,长任务需要显式进入后台模式,并持续保存状态。

Planning Engine:拆解目标,避免乱跑

规划引擎负责把目标拆成步骤,并动态更新。规划不一定非要由单独模型完成,初期可以让主模型输出一个简短计划。随着任务复杂度上升,可以独立出 planner。

一个好的计划应该满足四个要求:

  1. 可执行。每一步都能对应到工具或明确的分析动作。
  2. 可验证。每一步完成后知道如何判断成功。
  3. 可调整。工具结果变化后,计划可以更新。
  4. 不过度。不要为一个简单问题生成十几步复杂计划。

比如用户说:"帮我检查这个 Python 项目为什么测试失败。"一个合理计划是:

text 复制代码
1. 查看项目结构和测试配置。
2. 运行测试命令复现失败。
3. 阅读失败日志定位文件和函数。
4. 查看相关源码和测试。
5. 修改最小范围代码。
6. 重新运行测试确认。
7. 总结原因和改动。

这个计划的好处是,每一步都有明确动作,不是泛泛地说"分析问题"。

Memory Engine:保存真正有用的信息

记忆引擎不要变成垃圾桶。很多 Agent 项目失败,就是因为把所有历史都叫"记忆",最后模型每次都收到一堆无关内容。

更好的做法是按用途分层:

类型 保存内容 生命周期 示例
短期记忆 最近对话和工具结果 当前任务 最近一次测试失败日志
工作记忆 当前计划和状态 当前任务 已读取 main.py,待检查 test_api.py
长期记忆 稳定偏好和规则 多任务复用 用户偏好中文回答、代码注释用中文
项目记忆 仓库结构和约束 某个项目 使用 Poetry、测试命令为 pytest

记忆还要支持"遗忘"。过期、错误、重复的信息应该被压缩或删除。否则长期运行后,Agent 会被旧信息污染。

Context Engine:把正确的信息放进模型上下文

上下文引擎是 Agent 工程里非常容易被低估的模块。模型输出质量很大程度上取决于输入质量,而输入质量不只是 prompt 写得好,还包括材料选得准。

以代码 Agent 为例,用户问"为什么登录接口测试失败",上下文引擎不应该把整个仓库都给模型,而应该优先找:

  1. 失败日志。
  2. 登录接口实现。
  3. 登录相关测试。
  4. 路由配置。
  5. 认证中间件。
  6. 最近修改记录。

这就需要结合搜索、AST、文件依赖、调用关系和历史工具结果。

一个简单的 Context Engine 可以先这样做:
flowchart LR A关键词搜索 --> B文件路径打分 B --> C读取片段 C --> D截断过长内容 D --> E拼装 Prompt

更复杂一点,可以加入:

  1. 向量检索。
  2. 代码符号索引。
  3. AST 节点摘要。
  4. RepoMap 文件关系图。
  5. 历史摘要压缩。
  6. 重要性排序。

注意,上下文压缩不是简单地"缩短文字",而是保留对当前决策有用的信息。比如错误日志里最重要的是异常类型、栈顶文件、断言差异,而不是完整重复日志。

Tool Router:工具的分发器和守门人

Tool Router 负责把模型请求的工具调用分发给真正的工具实现。它不仅是路由器,也是守门人。

它需要做的事包括:

  1. 检查工具是否存在。
  2. 校验参数是否合法。
  3. 判断权限是否允许。
  4. 决定是否需要用户确认。
  5. 选择本地执行还是远程执行。
  6. 统一记录调用日志。
  7. 统一包装返回结果。

比如模型请求执行:

json 复制代码
{
  "tool": "shell",
  "args": {
    "cmd": "rm -rf /"
  }
}

一个合格的 Tool Router 必须拦截它,而不是天真地执行。Agent 的能力越强,Tool Router 越重要。

Sandbox Layer:安全边界

沙箱层负责限制工具的执行环境。没有沙箱的 Agent 就像一个拿着管理员权限的实习生,能干活,但风险极高。

常见沙箱分三类:

  1. Process Sandbox。通过进程权限、工作目录、超时、环境变量限制执行。
  2. Docker Sandbox。把任务放进容器,限制文件系统、网络、CPU、内存。
  3. Remote Sandbox。把高风险或重负载任务放到远程隔离环境。

对于个人项目,最小沙箱策略可以是:

  1. 默认只允许读写当前工作目录。
  2. Shell 命令必须有超时。
  3. 删除、覆盖、网络请求、安装依赖需要确认。
  4. 环境变量和密钥默认不暴露给模型。
  5. 工具返回结果做长度限制,避免日志塞爆上下文。

安全不是上线前才考虑的事。Agent 从第一天就应该有边界。

Provider Manager:屏蔽不同模型供应商差异

不同模型供应商的 API、工具调用格式、流式输出、上下文长度、价格、速度都不同。Provider Manager 的职责是把这些差异封装起来,让 Agent Runtime 只面对统一接口。

统一接口可以长这样:

text 复制代码
输入:messages + tools + model_config
输出:final_answer 或 tool_call

这样你就可以根据任务选择模型:

  1. 简单问答用便宜快速的模型。
  2. 复杂规划用更强的推理模型。
  3. 本地隐私任务用 Ollama。
  4. 多模态任务用支持图像或语音的模型。
  5. 代码任务用代码能力更强的模型。

一个成熟 Agent 不应该被单一模型绑定死。模型会变化,价格会变化,能力也会变化,架构上留出 provider 抽象是值得的。

Tool System:Agent 的手和脚

工具系统是 Agent 真正接触世界的地方。常见工具包括:

  1. File:读写文件、列目录、检查文件是否存在。
  2. Shell:执行命令、运行脚本、安装依赖。
  3. Git:查看 diff、提交记录、分支、状态。
  4. Search:搜索文本、联网检索、查文档。
  5. RepoMap:生成仓库结构和文件关系。
  6. AST:解析代码符号、函数、类、调用关系。
  7. Test:运行单元测试、集成测试、lint、类型检查。

工具越多,Agent 越强,但工具越多也越容易混乱。所以工具设计要遵循"少而清晰"的原则。每个工具都应该职责单一、参数明确、返回结果稳定。

4.3 一次完整任务的运行时序

下面用一个"让 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 的本质:它不是一次回答,而是一组带状态的动作链。

4.4 最小可用 Agent:用 Python 实现一个能调用工具的原型

下面我们用 Python 写一个最小 Agent。它不依赖真实大模型,而是用一个规则模型模拟"模型决策",这样你可以直接理解架构逻辑。真实项目里,只需要把 RuleBasedProvider 换成真正的 LLM Provider 即可。

这个示例支持两个工具:

  1. list_files:列出当前目录文件。
  2. read_file:读取指定文件。

它的目标不是炫技,而是把 Agent 的核心循环讲清楚。

python 复制代码
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 的核心结构:

  1. AgentRuntime 负责循环。
  2. Provider 负责决策。
  3. ToolRegistry 负责工具路由。
  4. FileTools 负责真实执行。
  5. AgentState 负责保存任务状态。
  6. max_rounds 负责防止无限循环。

你可以把它看作 Agent 的"骨架"。后面所有复杂能力,都是在这个骨架上加模块。

4.5 把规则模型换成真实大模型时,要注意什么

接入真实大模型之后,核心变化是:RuleBasedProvider.next_action() 不再用 if 判断,而是调用模型 API,让模型返回结构化结果。结构化结果通常有两类:

  1. 最终回答。
  2. 工具调用请求。

为了让模型输出稳定,建议让它返回类似下面的 JSON:

json 复制代码
{
  "type": "tool_call",
  "tool": "read_file",
  "args": {
    "path": "README.md"
  },
  "reason": "需要读取项目说明来理解启动方式"
}

或者:

json 复制代码
{
  "type": "final",
  "answer": "我已经完成检查,问题原因是测试配置缺少环境变量。"
}

模型输出越结构化,Runtime 越容易控制。不要让模型自由输出"我觉得应该调用 read_file 工具,参数大概是 README.md",因为这种文本很难稳定解析。

一个 Provider 抽象可以这样设计:

python 复制代码
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,上层逻辑就能继续工作。

系统提示词也要尽量工程化。例如:

text 复制代码
你是一个本地 Agent 助手,目标是帮助用户完成任务。

规则:
1. 如果需要外部信息,必须通过工具获取,不要猜测。
2. 每次只能调用一个工具,除非系统明确允许并行调用。
3. 高风险操作必须说明原因,并等待用户确认。
4. 如果工具返回失败,要根据失败原因调整计划。
5. 当信息足够时,输出最终答案。

输出格式:
如果需要工具,返回 JSON:
{"type":"tool_call","tool":"工具名","args":{},"reason":"原因"}

如果任务完成,返回 JSON:
{"type":"final","answer":"最终答案"}

这里的重点不是 prompt 多长,而是边界清楚、格式稳定、责任明确。

4.6 工具系统的设计示例:Shell 工具为什么一定要谨慎

文件工具相对安全,Shell 工具就危险得多。因为 Shell 能做的事情太多:删除文件、访问网络、读取密钥、启动进程、安装依赖、修改系统配置。给 Agent 接 Shell 工具之前,必须先加安全限制。

下面是一个简化版 Shell 工具,只允许执行白名单命令,并且设置超时。

python 复制代码
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)

真实项目里还应该继续增强:

  1. 限制工作目录。
  2. 屏蔽敏感环境变量。
  3. 禁止重定向到系统路径。
  4. 对删除、移动、覆盖等命令加二次确认。
  5. 支持 dry-run。
  6. 对每次执行记录审计日志。
  7. 对网络访问做开关控制。

不要因为 Agent "看起来聪明"就放松安全。越聪明的 Agent,越需要明确的权限边界。

4.7 记忆系统的一个简单实现

最小记忆系统可以用 JSONL 文件实现。JSONL 的特点是一行一个 JSON,追加写入很方便,也方便调试。

python 复制代码
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("中文"))

这个实现非常简单,但已经具备三个关键点:

  1. 能保存。
  2. 能检索。
  3. 能限制返回数量。

后续可以升级为 SQLite、向量数据库、全文索引,甚至把记忆分成用户级、项目级、任务级。但在早期,不要过度设计。先把"记什么、什么时候记、怎么用"想清楚,比一上来接复杂数据库更重要。

4.8 上下文压缩:让模型看到重点

当工具返回大量内容时,不能直接全部塞给模型。比如一个测试日志可能有几万行,一个代码仓库可能有几千个文件。如果不压缩,上下文会很快爆掉,模型也会抓不到重点。

一个最小的压缩策略可以是:

python 复制代码
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}"

但对不同内容,应该使用不同压缩策略:

  1. 错误日志:优先保留异常类型、栈顶、失败断言、退出码。
  2. 代码文件:优先保留函数签名、类结构、相关函数体。
  3. 搜索结果:优先保留标题、摘要、链接、发布时间。
  4. 对话历史:优先保留用户目标、已完成动作、未解决问题。
  5. 测试报告:优先保留失败用例、错误信息、复现命令。

也就是说,压缩不是机械截断,而是围绕当前目标保留决策所需信息。

4.9 一个可扩展的目录结构

如果你准备把 Agent 做成长期项目,可以从下面这个目录结构开始:

text 复制代码
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

这个结构的原则是"运行时和工具分离、模型和业务分离、安全策略单独管理"。这样后面扩展时不会把所有代码塞进一个巨大文件。

4.10 生产化 Agent 还需要哪些能力

最小 Agent 能跑起来,不代表能稳定上线。生产化至少要补齐下面这些能力:

  1. 日志。记录每轮输入、输出、工具调用、耗时、错误。
  2. 追踪。能查看一次任务完整执行链路。
  3. 评测。用固定任务集测试 Agent 是否退化。
  4. 权限。不同用户、不同工具、不同环境有不同权限。
  5. 审批。高风险动作必须人工确认。
  6. 回滚。文件修改、数据库写入、配置变更要能撤销。
  7. 成本控制。限制最大轮数、最大 token、最大工具调用次数。
  8. 并发控制。避免多个 Agent 同时修改同一资源。
  9. 失败恢复。进程中断后能恢复任务。
  10. 隐私保护。敏感数据不要直接送给外部模型。

很多 Agent Demo 看起来很酷,但一到真实环境就容易翻车,原因往往不是模型不够强,而是这些工程能力缺失。

4.11 Function Calling、Agents SDK 和 MCP 的分工

很多初学者会把这三个概念混在一起,其实它们解决的问题不一样。

Function Calling

Function Calling 解决的是"模型如何请求一次具体动作"。模型看到任务后,决定要调用哪个函数、传什么参数,应用程序再把这个函数真正执行掉。它更像"单次动作接口"。

适合的场景包括:

  1. 查天气。
  2. 读文件。
  3. 调接口。
  4. 搜索资料。
  5. 执行小型、明确、可验证的动作。

Agents SDK

Agents SDK 解决的是"谁来编排整个任务循环"。官方文档把 Agent 描述为:会规划、会调用工具、能协作、并且能保留足够状态来完成多步工作的应用。换句话说,SDK 更关注运行时、状态、审批、工具执行和多步工作流。

如果你的应用需要自己掌控:

  1. 任务编排。
  2. 工具执行。
  3. 审批流程。
  4. 会话状态。
  5. 多智能体协作。

那就更适合引入这类运行时框架,而不是只做一次函数调用。

MCP

MCP 解决的是"如何用统一协议连接外部系统"。它更像一层标准化连接方式,负责把数据源、工具、工作流暴露给 AI 应用,让不同客户端和不同服务器之间能以统一方式协作。

你可以把三者理解成一条链:

  1. Function Calling 负责单次动作。
  2. Agents SDK 负责整个任务编排。
  3. MCP 负责把外部工具和系统标准化接入。

一个简化的关系图如下:
flowchart LR User用户目标 --> RuntimeAgent Runtime Runtime --> Model大模型 Model -->|请求函数| FCFunction Calling Runtime -->|编排任务| SDKAgents SDK / 自研 Runtime Runtime -->|连接外部系统| MCPMCP Server MCP --> Tools文件/数据库/搜索/工作流

对于个人项目来说,最实用的路线通常是:

  1. 先用 Function Calling 跑通一个最小闭环。
  2. 再把 Runtime、状态和规划拆开。
  3. 最后接入 MCP,把外部能力标准化。

这样做的好处是,学习曲线更平滑,系统也更容易演进。

5.总结

Agent 助手的本质,不是"给聊天机器人加几个工具",而是构建一个围绕目标持续运行的任务系统。它需要模型的推理能力,也需要传统软件工程里的状态管理、权限控制、错误处理、日志追踪和模块化设计。

笔者从概念、原理、架构到代码示例,完整梳理了一个 Agent 助手的实现路径。你可以从最小版本开始:一个 Runtime、一个 Provider、一个 ToolRegistry、几个安全工具、一个 JSONL 记忆文件。等它跑稳之后,再逐步加入规划引擎、上下文引擎、沙箱、模型供应商管理、多 Agent 协作和评测系统。

对初学者来说,最重要的不是一开始就做一个"全自动超级智能体",而是先理解闭环:
flowchart LR A目标 --> B计划 B --> C工具调用 C --> D观察结果 D --> E反思修正 E --> F最终交付

只要这个闭环跑通,你就已经跨过了从"会聊天"到"会办事"的门槛。

未来 Agent 的前景非常明确。它会从单纯的问答入口,逐步变成个人工作台、开发环境、企业流程系统和自动化基础设施的一部分。它不会替代所有软件,但会重塑很多软件的交互方式:用户不再只点击按钮,而是直接描述目标;系统不再只展示信息,而是主动规划和执行。

不过,越是强大的 Agent,越需要可控。真正值得信任的 Agent,不是永远自信地往前冲,而是在需要信息时会查证,在需要操作时会申请权限,在失败时会修正路线,在完成后能给出清晰证据。

如果要用一句话收尾,那就是:

Agent 的未来不是"模型自己统治一切",而是"模型成为软件系统中会推理、会协作、会调用工具的核心执行单元"。

6.结束语

这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

另外,博主出新书了《Hadoop与Spark大数据全景解析》、同时已出版的《深入理解Hive》、《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》也可以和新书配套使用,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。

相关推荐
十正1 小时前
Claude code源码精读之上下文压缩
ai·aigc·agent·claude code
太阳上的雨天1 小时前
任何格式的文件转Markdown
python·ai
提笔了无痕1 小时前
RAG存储策略中.md格式的切片与存储怎么处理
数据库·ai·rag
my烂笔头1 小时前
单阶段 双阶段 目标检测的区别
人工智能·ai
weixin_468466852 小时前
纳米 AI 搜索新手极速上手指南
人工智能·python·深度学习·搜索引擎·ai·语言模型·自然语言处理
YueJoy.AI2 小时前
AI应用的API安全:从认证到授权的完整指南
人工智能·ai·语言模型
周易宅2 小时前
CLAUDE.md 与 MEMORY.md:AI 编程助手配置的两条平行铁轨
人工智能·ai·agent·claude
不懂的浪漫3 小时前
Role Agent 方法论:如何把一个标准工作流 Agent 化
人工智能·ai·agent
Bruce_Liuxiaowei3 小时前
2026年5月第5周网络安全形势周报
人工智能·安全·web安全·ai·智能体