【第三部分:第一个 Agent 应用】10. 不使用框架,手写一个最小 Agent

简介: 本文通过手写一个最小 Agent,拆解 Agent Loop 的核心机制:模型基于 Messages 判断下一步,必要时通过 Function Calling 选择 Tool,程序执行后将 Tool Result 作为 Observation 写回上下文,再次交给模型继续决策,直到任务完成。文章结合天气查询与计算示例,说明终止条件、异常处理和执行轨迹的重要性,并进一步对照 DeepSeek Harness,展示最小 Loop 如何逐步演进为具备 Session、Tool Registry、权限、恢复和可观测能力的 Agent Runtime。

前面的文章,我们已经分别介绍了 Context Engineering、Function Calling、Structured Output 和 RAG。这些能力单独看并不复杂,但真正进入 Agent 开发后,还需要回答一个问题:

谁来决定什么时候调用模型、什么时候执行工具、拿到工具结果以后是否继续,以及什么时候结束任务?

答案就是 Agent Loop。

从这一篇开始,我们正式进入“第一个 Agent 应用”。先不使用 LangChain、LangGraph、Spring AI 或其他 Agent Framework,而是用最基础的模型 API,手写一个能够“思考—调用工具—观察结果—继续执行”的最小 Agent。因为只有自己实现一次,才真正知道框架帮我们做了什么。


一、普通大模型调用和 Agent 到底差在哪里

最普通的大模型应用通常只有一次请求:

用户
 ↓
Prompt
 ↓
LLM
 ↓
Answer

image.gif

例如:用户:北京今天适合户外活动吗?模型生成一段回答,请求结束。但如果模型不知道实时天气,就需要调用天气服务:

用户
 ↓
LLM
 ↓
get_weather("北京")
 ↓
天气 API
 ↓
结果返回 LLM
 ↓
LLM 再次判断
 ↓
最终回答

image.gif

如果任务更复杂,还可能继续调用第二个、第三个工具。因此 Agent 真正增加的是一个循环:

while 任务未完成:
    调用模型
    判断是否需要工具
    执行工具
    把结果放回上下文

image.gif

这个 while,就是最小 Agent 的骨架。


二、最小 Agent 其实只需要五个组件

先把各种框架、Memory、RAG、多 Agent 全部放到一边,一个能够运行的最小 Agent 只需要:

组件 作用
LLM Client 调用大模型
Messages 保存当前上下文
Tool Registry 注册 Agent 可以使用的工具
Tool Executor 执行模型选择的工具
Agent Loop 控制模型与工具循环执行

整个结构可以压缩成:

image.gif 编辑

也就是前面介绍 ReAct 时看到的工作循环,现在第一次真正落实到代码中。


三、先定义 Tool:Agent 能做什么必须由程序决定

假设我们的最小 Agent 只有两个工具:

get_weather
查询城市天气
calculator
执行数学计算

image.gif

不要让模型自己“发明”工具。程序需要显式注册:

public interface Tool {
    String name();
    String description();
    JsonNode parameters();
    String execute(JsonNode arguments) throws Exception;
}

image.gif

例如天气工具:

public class WeatherTool implements Tool {
    @Override
    public String name() {
        return "get_weather";
    }
    @Override
    public String description() {
        return "查询指定城市的实时天气";
    }
    @Override
    public JsonNode parameters() {
        return Json.parse("""
        {
          "type": "object",
          "properties": {
            "city": {
              "type": "string"
            }
          },
          "required": ["city"]
        }
        """);
    }
    @Override
    public String execute(JsonNode args) {
        String city = args.get("city").asText();
        // 实际项目中调用天气 API
        return """
        {
          "city": "%s",
          "weather": "小雨",
          "temperature": 26
        }
        """.formatted(city);
    }
}

image.gif

这里其实已经用到了前面介绍的 Function Calling:

名称告诉模型调用哪个能力,Description 帮助模型判断什么时候使用,JSON Schema 约束参数结构,真正的执行逻辑仍然由程序负责。


四、Tool Registry:不要写一大堆 if / else

工具越来越多以后,如果这样处理:

if ("get_weather".equals(name)) {
    ...
} else if ("calculator".equals(name)) {
    ...
}

image.gif

很快就会失控。最简单的方法是建立 Registry:

Map<String, Tool> tools = new HashMap<>();
tools.put("get_weather", new WeatherTool());
tools.put("calculator", new CalculatorTool());

image.gif

模型返回:

{
  "name": "get_weather",
  "arguments": {
    "city": "上海"
  }
}

image.gif

Agent Runtime 根据名称找到:

Tool tool = tools.get(toolCall.name());

image.gif

然后执行:

String result =
        tool.execute(toolCall.arguments());

image.gif

所以:

模型负责选择 Tool,程序负责确认 Tool 是否存在并真正执行。

这个责任边界非常重要。


五、真正的核心:手写 Agent Loop

有了模型和工具以后,Agent Loop 本身其实非常短:

record ToolCall( 
      String id, 
      String name, 
      Map<String, Object> arguments 
) {  } 
record ModelResponse( 
      String content, 
      List<ToolCall> toolCalls 
) {  }

image.gif

public String run(String userInput) {
    List<Message> messages = new ArrayList<>();
    messages.add(Message.system(
        "你是一个可以调用工具完成任务的智能助手。"
    ));
    messages.add(Message.user(userInput));
    int maxSteps = 10;
    for (int step = 1; step <= maxSteps; step++) {
        ModelResponse response =
                llm.chat(messages, toolSchemas());
        // 先保存模型本轮输出
        messages.add(response.message());
        // 没有工具调用,说明模型准备返回结果
        if (response.toolCalls().isEmpty()) {
            return response.content();
        }
        // 执行所有工具调用
        for (ToolCall call : response.toolCalls()) {
            Tool tool = tools.get(call.name());
            if (tool == null) {
                messages.add(
                    Message.tool(
                        call.id(),
                        "ERROR: tool not found"
                    )
                );
                continue;
            }
            try {
                String result =
                        tool.execute(call.arguments());
                messages.add(
                    Message.tool(
                        call.id(),
                        result
                    )
                );
            } catch (Exception e) {
                messages.add(
                    Message.tool(
                        call.id(),
                        "ERROR: " + e.getMessage()
                    )
                );
            }
        }
    }
    throw new IllegalStateException(
        "Agent exceeded max steps"
    );
}

image.gif

暂时忽略各种 SDK 差异,这段代码已经是一个真正意义上的最小 Agent。它完成了:

调用模型
   ↓
读取 Tool Call
   ↓
找到工具
   ↓
执行工具
   ↓
获得 Observation
   ↓
写回上下文
   ↓
再次调用模型

image.gif

直到模型不再请求工具,循环结束。


六、跑一次完整任务,会发生什么

例如用户输入:

查询上海现在的天气,并计算 3 个人出差两天、每天每人交通费 120 元,总交通预算是多少?最后给我一个简短的出行建议。

第一次调用模型:

LLM
 ↓
get_weather(city="上海")

image.gif

Agent 执行天气工具:

{
  "city": "上海",
  "weather": "小雨",
  "temperature": 26
}

image.gif

然后把 Tool Result 放回 Messages。

第二次调用模型时,它已经知道天气,但还需要计算:

LLM
 ↓
calculator("3 * 2 * 120")

image.gif

工具返回:

720

image.gif

再次写回上下文。

第三次调用模型:

LLM
 ↓
已经拥有:
用户目标
+ 天气结果
+ 计算结果
 ↓
最终回答

image.gif

最终可能得到:

上海目前有小雨,建议携带雨具。3 人出差 2 天,按每天每人 120 元计算,交通预算共 720 元。

整个执行过程可以画成:

image.gif 编辑

这里没有预先写死:

先查天气
再算费用
最后回答

image.gif

真正决定下一步的是模型。这也是 Agent 与固定 Workflow 最核心的区别之一。


七、为什么 Tool Result 一定要重新交给模型

这是第一次写 Agent 时很容易犯的错误。有人会认为:

模型调用天气工具
 ↓
天气工具返回结果
 ↓
直接把天气结果返回用户

image.gif

这样做实际上切断了 Agent Loop。Tool 的结果不是最终答案,而是:

模型下一轮决策需要观察到的新信息。

因此:

Tool Result
     ↓
加入 Messages
     ↓
再次调用 Model

image.gif

这就是 ReAct 中的:Observation。模型看到 Observation 后,才能判断:

任务完成了吗?
还缺什么信息?
需要再调用其他工具吗?
是否可以生成最终结果?

image.gif

所以 Agent Loop 不是简单的“模型调用工具”,而是:

Model → Action → Observation → Model

形成闭环。


八、什么时候结束循环?

最简单的终止条件是:

模型没有再返回 Tool Call

image.gif

也就是:

if (response.toolCalls().isEmpty()) {
    return response.content();
}

image.gif

但生产系统不能只相信模型自己会停下来。至少还需要:

最大 Step
工具调用次数
任务超时
Token Budget
用户取消
异常终止

image.gif

例如:

int maxSteps = 10;

image.gif

这样即使模型出现:

调用 A
 ↓
调用 B
 ↓
又调用 A
 ↓
又调用 B

image.gif

也不会无限执行。因此:

Agent Loop 必须拥有程序层面的终止条件,而不能把“什么时候结束”完全交给模型。


九、工具失败时,不一定应该立即结束 Agent

假设天气 API 超时。一种实现是:

Tool Error
 ↓
整个任务失败

image.gif

但 Agent 更有价值的地方是,可以把错误本身作为 Observation:

{
  "error": "weather service timeout"
}

image.gif

再交给模型。模型可能决定:

重试
换一个工具
跳过这个步骤
告诉用户当前无法获取天气

image.gif

所以更加合理的是:

try {
    result = tool.execute(...);
} catch (Exception e) {
    result = "ERROR: " + e.getMessage();
}
messages.add(toolResult);

image.gif

当然,这不代表可以无限重试。仍然需要:Retry Limit + Timeout + Max Steps 来兜底。


十、日志真正应该记录什么

普通 API 日志通常只记录:

Request
Response
Latency

image.gif

Agent 不够。因为一次用户请求可能经历很多 Step。至少应该记录:

Run
 ├─ Step 1
 │   ├─ Model Request
 │   ├─ Tool Call
 │   └─ Tool Result
 │
 ├─ Step 2
 │   ├─ Model Request
 │   ├─ Tool Call
 │   └─ Tool Result
 │
 └─ Step 3
     └─ Final Answer

image.gif

例如:

runId     = r-001
step      = 2
tool      = calculator
arguments = 3*2*120
result    = 720
latency   = 8ms

image.gif

否则用户只说:

“这个 Agent 算错了。”

开发人员很难知道到底是:模型选错工具、参数生成错误、工具执行错误,还是模型理解 Tool Result 时出错。这也是为什么后面进入生产级 Agent 后,“Trace”会比普通日志更加重要。


十一、这个最小 Agent 已经包含了哪些前置知识

回头看前面的文章,会发现我们其实已经把一个 Agent 所需的基础零件全部准备好了。

Context Engineering

负责这一轮应该把什么信息交给模型。在当前代码里就是:

List<Message> messages;

image.gif


Function Calling

负责模型怎样表达自己想调用哪个工具。对应:

Tool Call
name
arguments

image.gif


Structured Output

负责模型怎样按照确定的数据结构与程序通信。Tool 参数本身就是一种 Structured Output。


RAG

如果再注册一个:

search_knowledge

image.gif

工具:

Agent
 ↓
search_knowledge
 ↓
RAG
 ↓
相关知识
 ↓
Tool Result
 ↓
Agent

image.gif

RAG 就自然进入了 Agent Loop。所以前面几篇并不是相互独立的技术点。进入这一篇以后,它们第一次真正连接起来。


十二、到这里,其实已经能看懂 Agent Framework 在做什么

把我们的代码继续完善,很快会出现更多需求:

工具越来越多
Messages 越来越长
任务需要恢复
工具需要权限
需要流式输出
需要并行 Tool Call
需要审批
需要 Retry
需要日志和 Trace
需要 Session
需要 Sandbox

image.gif

于是最开始几十行的:

while(...)

image.gif

会逐渐变成一个完整 Runtime。这就是 Agent Framework 和 Agent Harness 开始出现的原因。


十三、再看 DeepSeek Harness:复杂 Harness 的核心仍然是这个 Loop

最近 DeepSeek 开源了官方 DeepSeek Harness(dsh)。它目前仍处于 Developer Preview,官方明确提醒正在快速迭代,并可能出现破坏兼容性的变化;架构上采用 “Everything is a Plugin” 的方式,由 Cordis 驱动,Model Adapter、Tool Registry、Session Log,甚至 Agent Loop 本身都作为可替换组件存在。它的核心结构可以简化理解成:

Session Log
     ↓
System Prompt
+
Tool Schemas
+
History
     ↓
Agent Loop
     ↓
LLM Adapter
     ↓
Assistant Message
     ↓
Tool Call
     ↓
Tool Registry
     ↓
Tool Execute
     ↓
Tool Result
     ↓
Session Log
     ↓
下一 Step

image.gif

DeepSeek Harness 官方架构文档对一次 Turn 的描述也非常接近这个过程:从 Session Log 取得输入并组装 Prompt 和 Tool Schema,调用模型,将 Tool Call 送入工具执行管线,再把模型可见的结果写回日志,并据此决定是否进入下一 Step。与我们前面几十行代码相比:

手写最小 Agent DeepSeek Harness
List<Message> Session Event Log
Map<String, Tool> Scoped Tool Registry
for / while Agent Loop
llm.chat() LLM Adapter
Tool 执行 Guarded Tool Pipeline
try/catch Error / Recovery Extension Points
直接拼 Prompt System Prompt Assembly
内存上下文 可持久化、可回放 Session

也就是说,虽然代码复杂度完全不是一个量级,但最底层的逻辑并没有改变:

获取上下文 → 调用模型 → 执行 Action → 记录 Observation → 再次决策。


十四、DeepSeek Harness 最值得初学者关注的,不是插件有多少

如果现在直接研究 DeepSeek Harness 的全部代码,很容易被 Plugin、Cordis、Event、Session、Scope、Sandbox 等大量工程概念淹没。对于刚开始学习 Agent 的开发者,更值得关注的是它背后的几个设计变化。

第一,从 Messages List 变成 Session Log

我们的 Demo 直接维护:

List<Message>

image.gif

复杂系统则需要解决:恢复、回放、分支、Trace、持久化和上下文重建。DeepSeek Harness 因此把 Session Log 作为模型上下文的事实来源,下一次模型请求从日志中重新推导 History。

第二,从 Tool Map 变成 Tool Registry

Demo 中:

Map<String, Tool>

image.gif

就够了。但生产系统还需要:Tool Scope、权限、Schema、执行前检查、执行后处理和 Sandbox。于是简单工具表逐渐演变成工具执行管线。

第三,从 while 循环变成 Agent Runtime

我们的:

for (step = 0; step < maxSteps; step++)

image.gif

最终需要处理:Turn、Step、取消、继续、错误恢复、输入插入、状态持久化以及多种执行扩展点。Agent Loop 于是逐渐从一段控制代码,演变成 Runtime 的核心。

image.gif 编辑

这也解释了为什么本系列后面还会单独讨论 Agent Harness。


十五、最小 Agent 不应该解决哪些问题

这一篇的目标不是写一个“生产级 Agent”。因此暂时不应该把:

Memory
Multi-Agent
Graph Workflow
MCP
A2A
Sandbox
复杂 Planning
长期任务
Checkpoint

image.gif

全部塞进来。否则读者最终看到的又会是另一个框架。最小 Agent 的真正价值,是把核心机制暴露出来:

messages
+
tools
+
model
+
loop

image.gif

只要理解了这四部分,后面无论使用 Spring AI、LangChain4j、LangGraph,还是阅读 DeepSeek Harness,都可以继续追问:

这个框架把我的 Messages 放在哪里?

Tool Registry 在哪里?

谁负责执行 Agent Loop?

Tool Result 怎样进入下一轮模型上下文?

终止、错误和恢复由谁负责?

这比记住某个框架的 API 更重要。


十六、小结

前面的文章一直在拆解 Agent:

Context
Function Calling
Structured Output
RAG

image.gif

这一篇第一次把它们重新组合起来。因此:

Agent 并不是某个框架提供的特殊对象,Agent 首先是一种运行机制。

模型提供理解和决策能力;Tool 提供外部能力;Context 保存当前状态;而 Agent Loop 把它们持续连接起来。

DeepSeek Harness 这样的工程化项目进一步说明,当这个简单 Loop 需要支持 Session、权限、Sandbox、恢复、扩展和可观测性时,它就会逐渐演变成完整的 Agent Harness。官方当前实现甚至把 Agent Loop 本身也设计成可替换插件,说明“Loop 是核心,但不应该成为不可扩展的硬编码核心”。

上一篇回顾:

https://developer.aliyun.com/article/1756061?spm=a2c6h.13148508.setting.15.31fa4f0eYl1atu

下一篇将真正进入使用主流框架开发 Agent:

到时候我们再来看 Spring AI、LangChain4j、LangGraph 等框架究竟帮助开发者封装了什么。

相关文章
|
2月前
|
人工智能 算法 API
【第二部分:大模型应用开发基础】9. RAG 是什么,它与 Agent 有什么关系?——从知识库问答到 Agentic RAG
RAG 通过文档解析、切分、Embedding、混合检索、Rerank 与引用机制,让大模型在回答问题时能够按需获取企业知识,而不是依赖训练数据“记住一切”。文章进一步介绍 RAG 如何从固定的检索增强生成流程演进到 Agentic RAG:由 Agent 判断是否需要检索、如何规划 Query、证据是否充分,并在必要时继续改写和多轮检索。同时梳理 RAG、Memory、Tool 与 Agent 的边界,强调知识库问答系统并不等同于 Agent,RAG 只是 Agent 获取外部知识的一种能力。
312 2
|
2月前
|
人工智能 Java API
【第三部分:第一个 Agent 应用】11.Spring AI、LangChain4j、LangGraph 到底帮我们封装了什么?
本文承接“手写最小 Agent”,从框架抽象层级出发,分析 Spring AI、LangChain4j、LangGraph 等主流 Agent Framework 究竟封装了什么。Spring AI 更适合将 AI 能力融入 Spring 应用,LangChain4j 强调 Java Service 化与类型化开发,LangGraph 则擅长状态、分支、循环、检查点和长任务编排。文章同时结合 Qwen-Agent、AgentScope 等国内开源框架,说明 Agent Framework 并没有改变 Agent Loop 的本质,而是在其外围补齐 Memory、RAG、State、Retry、
311 2
|
2月前
|
设计模式 Web App开发 人工智能
【AI】Agent 全栈进阶|系统化学习路线专题
描述 Agent 的概念、核心构成,规划出一套循序渐进的 Agent 开发学习路径,从大模型调用、工具调用、RAG、运行模式、记忆机制再到工程化调试,同时附上多款适合入门钻研的开源参考项目
1869 7
|
2月前
|
JSON 自然语言处理 Java
【第二部分:大模型应用开发基础】8.Structured Output——让模型输出可被程序可靠处理的数据
Structured Output 是 Agentic AI 连接大模型与业务系统的重要基础能力。相比仅要求模型返回 JSON,它进一步通过 JSON Schema、DTO、运行时校验和业务规则校验,把概率性的模型输出转化为程序可解析、可验证、可执行的数据契约。文章结合 OpenAI 原生 Structured Outputs 与 DeepSeek JSON Output,介绍枚举、日期、金额、嵌套对象、异常修复、有限重试及 Java/TypeScript 类型映射,并强调:JSON 合法、Schema 合法并不等于业务合法,生产级 Agent 必须在结构化输出之后继续进行业务与权限校验。
185 0
【第二部分:大模型应用开发基础】8.Structured Output——让模型输出可被程序可靠处理的数据
|
2月前
|
缓存 文字识别 安全
【第二部分:大模型应用开发基础】6. Prompt Engineering 与 Context Engineering:生产级 Agent 如何管理上下文
本文从 Agent 开发实践出发,分析 Prompt Engineering 的作用与边界,说明复杂 Agent 为什么不能依赖超长 Prompt,并系统介绍 Context Engineering 对系统指令、用户目标、业务数据、工具结果、历史消息和任务状态的动态选择、组装、裁剪与压缩。文章结合合同审查 Agent 与 Java 示例,进一步讨论 Context Rot、Token 预算、Prompt Cache、可观测性及 Prompt Injection 防护,帮助开发者建立生产级 Agent 的上下文管理思路。
218 1
|
1月前
|
自然语言处理 搜索推荐 关系型数据库
【第三部分:第一个 Agent 应用】12. 使用状态机开发一个可控 Agent
本文围绕“使用状态机开发一个可控 Agent”展开,以需求分析 Agent 为例,介绍 State、Node、Edge、Conditional Edge、Checkpoint、Interrupt、Resume 与 Human-in-the-Loop 等核心机制,并通过 LangGraph 代码说明如何把自由 Agent Loop 收束为可分支、可循环、可暂停、可恢复的业务流程。核心思想是:状态机负责确定性的流程控制,Agent 负责节点内部的理解、推理、RAG 与 Tool 调用,实现“确定性的骨架 + 概率性的智能”。
138 1
|
2月前
|
JSON 自然语言处理 前端开发
【第二部分:大模型应用开发基础】7.Function Calling:让大模型调用真实程序能力
Function Calling 让大模型从“生成文本”走向“调用真实程序能力”。模型根据工具名称、描述和 JSON Schema 选择工具、生成参数,应用程序再完成校验、鉴权、执行和结果回传。文章结合天气查询、销售数据与计算 Agent,说明并行调用、失败处理、前后端调用及权限控制,并梳理 Function Calling 与 ReAct、Tool、Skill、MCP 的关系:ReAct 负责执行循环,Tool 提供能力,Skill 沉淀方法,MCP 统一连接外部工具和数据
257 1
|
3月前
|
机器学习/深度学习 人工智能 资源调度
银行零售信贷AI实践:从尽调到贷后的全链路Skill化
信贷审批的本质不是批不批,而是还得起多少。本文详解等额本息与提前还款的算法实现,构建WOE+IV值信用评分模型实现自动审批,用RFM+K-Means聚类完成零售客户分层,设计基于余弦相似度+TopN的产品推荐和AUM增长测算,实现马科维茨投资组合优化和退休规划AI推演引擎。附带7大维度完整性检查清单。
|
3月前
|
人工智能 JSON 监控
GEO技术原理与实践:从结构化数据到AI引用率的工程化方案
本文深度解析GEO(生成式引擎优化)技术原理与落地实践,涵盖结构化数据标记(FAQ/HowTo/Article Schema)、语义相关性优化(向量检索、同义覆盖、三层内容结构)、可信度工程(E-E-A-T评分、Author/Citation Schema)及效果监测体系,助力内容被AI搜索高频引用。(239字)
|
7月前
|
Arthas 人工智能 Java
我们做了比你更懂 Java 的 AI-Agent -- Arthas Agent
Arthas Agent 是基于阿里开源Java诊断工具Arthas的AI智能助手,支持自然语言提问,自动匹配排障技能、生成安全可控命令、循证推进并输出结构化报告,大幅降低线上问题定位门槛。
2331 64
我们做了比你更懂 Java 的 AI-Agent -- Arthas Agent

热门文章

最新文章