一个本地 AI Agent 是怎么跑起来的

一个本地 AI Agent 是怎么跑起来的

从消息、工具到 Agentic Loop,拆开它的实现脉络

这篇文章不聊空泛概念,而是顺着一个本地 AI Agent 的真实执行链路,拆开它为什么会看起来像在"自主工作":从消息结构、流式事件,到工具系统、文件与 Bash 工具,再到 Agentic Loop、系统分层和运行时 Prompt。你会看到,Agent 的关键不在神秘感,而在这些朴素机制是如何被稳稳接起来的。

很多人第一次认真用本地 AI Agent,都会有一种很强的错觉。

你让它改一段代码,它先去读文件;读完觉得上下文不够,又去搜关键词;接着跑一遍测试,发现报错后再回头修改;最后它不仅把改动做完,还会顺手告诉你为什么这么改。整个过程看起来很像一个会自己推进任务的"数字同事"。

这种体验很容易让人把 Agent 想得很玄:好像模型突然从"会聊天"进化成了"会工作"。但如果把它拆开来看,你会发现里面并没有什么魔法。真正支撑这种"自主工作感"的,往往只是几层朴素机制被稳稳接在了一起:消息怎么表示,流式事件怎么传,工具怎么抽象,循环怎么编排,运行时上下文怎么喂给模型。

换句话说,Agent 之所以像在"自主工作",不是因为它突然拥有了神秘智能,而是因为消息协议、工具抽象、执行循环和运行时上下文被一层一层接成了闭环。

这篇文章不聊空泛概念,就顺着一个本地 Coding Agent 的真实执行链路,拆开它到底是怎么"活起来"的。

为什么 content 不能只是字符串

很多人刚开始做 Agent 时,会下意识沿用最简单的想象:assistant 回来一段文本,程序把这段文本显示出来,如果要调用工具,再额外设计一套机制。这种思路很像早期的"问一句,答一段",本质上默认 assistant 消息就是一个字符串。

问题在于,这种想象天然把"说话"和"行动"拆开了。但一个真正可用的 Agent,恰恰需要把这两件事放在同一条消息里。至少在 Anthropic 这一类协议里,assistant 的 content 更像是一个内容块数组,而不是一整段纯文本。

json 复制代码
{
  "role": "assistant",
  "content": [
    { "type": "text", "text": "让我帮你看看这个文件..." },
    { "type": "tool_use", "id": "toolu_01", "name": "Read", "input": { "path": "src/main.ts" } }
  ]
}

这个结构的关键不在于"格式更复杂",而在于它表达了一个完全不同的事实:模型不是先把话说完,再去调用工具;而是可以一边组织表达,一边发起动作。

这件事会直接影响后面的所有设计。因为从这一刻开始,你就不能再把一次响应理解成"一整段最终答案",而要把它理解成"由多个内容块拼成的过程消息"。文本块、工具块、后续补充,都在同一条消息流里出现。Agentic Loop 之所以成立,本质上就是建立在这种消息模型上。

很多 Agent 看起来忽然变聪明,第一步其实不是工具变多了,而是消息结构终于能承载"边说边做"这件事了。路是从这里开始铺的。

流式返回不是优化细节,而是运行底座

如果消息结构解决的是"模型能不能边说边做",那么流式事件解决的就是"程序怎么实时接住这件事"。

很多人一开始觉得 stream: true 只是体验优化,无非是让字早点蹦出来。但对 Agent 来说,流不是锦上添花,它更像一条装配线。因为模型输出的不是一整坨 JSON,而是一串按生命周期推进的事件。

text 复制代码
content_block_start
content_block_delta
content_block_stop
content_block_start
content_block_delta
content_block_stop
message_delta
message_stop

这串事件最重要的价值,是让程序知道"一块内容什么时候开始、什么时候完整"。文本块可以逐字展示,工具块的输入参数也不是一次性给全,而是随着 content_block_delta 一段一段拼出来;等到某个 content_block_stop 到来时,系统才知道这个工具调用终于成型,可以进入下一步处理。

于是,UI 能及时显示"正在调用工具",工具执行层知道何时接管,usage 统计也知道何时在 message_delta 收尾。看起来只是几种事件名,背后其实是在给整套系统分发节拍。

所以流式返回真正改变的,不是"快一点",而是 Agent 从"等结果"变成了"跟过程协作"。消息结构给了它动作语法,流式事件则给了它运行时节拍。

工具系统不是附属能力,而是行动接口

消息结构和流式事件,解决的是模型如何表达动作意图。接下来还差一件更现实的事:程序怎么把这个意图变成一个可执行、可治理、可复用的动作接口。

这就是工具系统存在的意义。

ts 复制代码
export interface Tool {
  readonly name: string
  readonly description: string
  readonly inputSchema: JSONSchema
  call(input, context): Promise<ToolResult>
  isReadOnly(): boolean
  isEnabled(): boolean
}

这个接口看起来很朴素,但里面其实分了三层职责。

namedescriptioninputSchema 是给模型看的。模型要先知道这是什么工具、什么时候该用、参数该怎么传,才谈得上正确调用。call() 是给程序执行的,决定这个工具真正做什么。isReadOnly()isEnabled() 则是给运行时治理的,前者关系到权限确认,后者关系到当前环境下是否应该暴露这个工具。

这也是为什么一个成熟 Agent 不会把所有事情都粗暴塞进 Bash。工具系统不是"程序员为了代码优雅而抽象出来的接口",它本质上是在给模型建立行动语义。边界越清楚,模型的行为越稳定;边界越混乱,模型越容易把所有事都变成一条不透明的 shell 命令。

所以,"搜内容"最好是 Grep,"找文件"最好是 Glob,"真要动系统命令"再交给 Bash。看起来是工程上的模块拆分,本质上是在教模型如何选择动作。短期图省事把搜索也塞进 Bash,当然能跑;但长期看,工具语义会越来越糊,权限控制也会越来越粗。

文件工具和 Bash,代表两种行动哲学

在所有工具里,最有代表性的通常是两类:文件工具和 Bash。前者追求精确,后者追求能力上限。

先看文件读取。一个实用的 Read 工具,绝不只是"给我个路径,我把全文吐出来"。因为真实代码库里,一个 5000 行文件全读进来,既浪费 token,也会让模型失去焦点。更好的做法,是给它精细的读取能力。

ts 复制代码
inputSchema: {
  type: "object",
  properties: {
    file_path: { type: "string" },
    offset: { type: "number" },
    limit: { type: "number" }
  },
  required: ["file_path"]
}

offsetlimit 的意义,说白了就是让模型学会"别上来把整本书翻完,先看目录,再翻到关键页"。它可以先读前 100 行判断结构,再去精准拿真正关心的 20 行。

而且读出来的结果最好带行号。因为对后续编辑来说,行号不是装饰,它是坐标系。没有行号,模型只能靠文本匹配去猜;有了行号,它才能更稳定地说出"第 82 到 88 行该怎么改"。如果说读取是在找材料,那么行号就是后续编辑时的定位尺。

文件工具的另一条隐形主线是路径安全。相对路径转绝对路径、展开 ~、校验目标是否仍落在工作目录内,这些逻辑最好集中封装,而不是散落在每个工具里重复写。否则一个不小心,Agent 读写范围就会溢出到工作区之外。

另一边的 Bash,则像工具层里最锋利也最危险的一把刀。

ts 复制代码
const child = spawn(shell, ["-lc", command], {
  cwd,
  env: process.env
})

之所以推荐基于 spawn(),不是因为写法更"底层",而是它更适合真正的运行时治理:你需要收集 stdout/stderr,需要支持超时,需要响应 AbortSignal,还要对超长输出做截断。因为 Bash 最怕的不是"能力不够",恰恰是能力太强,结果把系统拖进不可控状态。

所以一个好用的 Agent,不只是"工具多",而是知道什么时候该用精细动作,什么时候该动用重型动作。Read 像手术刀,Bash 像开山斧,真正成熟的系统不会让两者互相冒充。

Agentic Loop 才是 Agent 真正"活起来"的地方

如果前面的消息结构是在修路,工具系统是在造手脚,那么 Agentic Loop 才是真正让整套系统开始呼吸的循环系统。

它的本质其实非常朴素:模型决定下一步,程序执行动作,结果再回到上下文里,供模型继续推理。

text 复制代码
用户提问
  -> 调 API(带 tools)
    -> 模型返回 stop_reason="tool_use"
      -> 程序执行工具
        -> 把结果作为 tool_result 塞回消息数组
          -> 再调一次 API
            -> 直到 stop_reason="end_turn"

真正让人着迷的,就是这条链路一旦跑起来,系统会表现出一种连续推进任务的"自主感"。它不是回答一次就结束,而是在一轮内部不断经历"推理 -> 动作 -> 获取反馈 -> 再推理"的循环。你看到的"它先看看文件,再试一下,再修一下",其实就是这个循环在外部世界留下的动作轨迹。

这里还有一个很有意思的实现细节:tool_resultrole 通常是 user。乍看有点反直觉,但从 API 的视角看,工具执行结果本来就是"外部世界新提供给模型的信息"。模型说"帮我读这个文件",程序读完之后,再把文件内容作为新的上下文送回去,这在协议层面更像"用户侧补充了一条信息"。

也正因为如此,Agent 的"自主工作感"并不来自某一条神奇 Prompt。Prompt 最多只能约束风格和原则,真正让系统看起来会自己干活的,是这个闭环一直在跑,而且每次跑完都会把新状态回灌进上下文。

前面几层做的所有铺垫,到这里才真正合龙:消息结构负责承载动作,流式事件负责运送动作,工具系统负责执行动作,而 Agentic Loop 负责把这一切反复接起来。

成熟实现为什么最后都会长成分层架构

做到这里,很多人会发现:一个单轮 Agentic Loop 已经能工作了,为什么成熟系统最后还会拆出通信层、会话层、UI 层,甚至再补一个 QueryEngine?

答案很简单,因为"这一轮怎么跑完"和"一个会话怎么持续存在"根本不是一回事。

通信层只负责一次请求。它接消息、调模型、流式吐事件,最后返回完整的 assistant message 和 usage。它关心的是"如何和模型说话",不关心"这轮之后还要不要继续"。

Agentic Loop 负责单轮编排。它决定当前消息数组是什么、模型是要收尾还是要调工具、工具结果如何组织回 tool_result、什么条件下算这一轮结束。它是单轮自主循环的小发动机。

再往上一层,像 QueryEngine 这样的会话编排器,解决的是另一类问题:历史消息怎么累积、每轮 system prompt 怎么重建、总 usage 怎么累计、本地命令如何拦截、中断信号怎么管理。它不是在替 Loop 干活,而是在把一轮一轮串成一个真正的产品会话。

最后 UI 只负责消费事件和展示状态。收到文本就渲染,收到 tool_use_start 就展示"正在调用工具",收到本轮结束信号就更新 loading 和提示信息。它不该知道权限判定、轮次终止条件、工具查找这些核心逻辑。

这种分层一旦站稳,后面的权限系统、会话存储、成本追踪、摘要压缩,才会很自然地长出来。很多看似"高级功能",其实都是架构边界清楚之后的副产品。你会发现,成熟 Agent 的复杂感,往往不是来自某个超级模块,而是来自每一层都只做自己该做的事。

Prompt 不是开场白,而是运行时上下文

最后再看 Prompt,就会很容易跳出一个常见误区:system prompt 不是一段漂亮的"角色设定词",而是一份动态组装的运行时上下文。

一个成熟 Agent 每轮请求前,最好都重新组织一遍这份上下文:当前工作目录、日期、操作系统、Git 分支、工作区状态、最近一次提交,甚至权限模式、会话附加规则,都可以作为 section 拼进去。

为什么要这么做?因为 Agent 做的不是抽象问答,而是现场工作。模型如果知道自己当前在哪个目录、仓库是不是干净、最近一次提交在做什么,它对当前任务的判断会稳定很多。

这也是 Git 信息非常值得放进 Prompt 的原因。分支名能帮助模型理解当前任务语境,git status 能提醒它工作区不是干净白板,最近一次提交则常常能补上一条极有价值的项目背景。

很多时候,大家说"这个 Agent 怎么突然变聪明了",并不一定是模型换了,而是上下文终于更完整了。模型第一次真正知道自己身在何处、手里有什么、现在该谨慎还是该继续推进,这种"现场感"会直接改变它的行为质量。

如果说前面的工具、循环和分层是在给 Agent 长手脚、长神经,那么运行时上下文做的,就是让它别在黑暗里摸索。

所谓 Agent,不过是被接好的骨架

回头看,一个能工作的本地 AI Agent,其实并不神秘。

消息结构决定它能不能边说边做,流式事件决定它能不能实时协作,工具系统决定它拥有什么行动接口,文件工具和 Bash 决定它如何在精确与能力上限之间分工,Agentic Loop 决定它能不能持续推进任务,而 Prompt 与环境上下文,则决定它到底有没有"身在现场"的工作感。

所以做 Agent 最值得下功夫的地方,往往不是追逐某种更玄妙的"智能感",而是把这些基础结构搭对。因为只有骨架立住了,权限、记忆、成本控制、子 Agent 协作这些能力,才会一个个自然长出来。

很多时候,所谓"Agent 突然变聪明了",不过是因为你终于给了它一副像样的身体。

相关推荐
该用户已不存在3 小时前
Claude Mythos 发布,强到刚出道就被雪藏?
aigc·ai编程·claude
飞龙14775657467503 小时前
从零创建 skill:Skill Creator 项目全解析
agent
房贷压不垮的码农3 小时前
5 分钟极速入门:用 Python 和 ChromaDB 体验向量数据库的魅力
ai编程
Flittly4 小时前
【SpringAIAlibaba新手村系列】(18)Agent 智能体与今日菜单应用
java·spring boot·agent
袋鱼不重5 小时前
Hermes Agent 安装与实战:从安装到与 OpenClaw 全方位对比
前端·后端·ai编程
石工记5 小时前
Agent 应用与图状态编排框架LangGraph
python·ai编程
程序员鱼皮5 小时前
SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程
ai·程序员·开源·编程·ai编程
踩着两条虫5 小时前
目录:VTJ.PRO 在线应用开发平台技术揭秘
vue.js·低代码·ai编程