我是怎么把一个普通 AI 聊天项目改造成工程化 Agent Runtime 的
大二,学 Java 后端,想做点和 AI 有关的项目,然后 fork 了一个现成的面试平台。
结果就一发不可收拾地折腾了好几个月,把它改造成了一个有 Trace、Memory、Approval、Guardrail、多步预算控制的工程化 Agent 系统。
这篇文章就是想聊聊这个过程------我是怎么想的,踩了哪些坑,以及什么设计让我觉得"这才对"。
起点:一个"会调 API 的聊天页"
说实话,一开始 fork 下来的项目,AI 能力就是:把用户消息塞进 Prompt,调一下大模型,把回复展示出来。
这没什么问题,能跑,功能也完整------简历解析、模拟面试、知识库问答都有。
但它有一个根本性的问题:模型输出直通执行,没有任何 Agent 化的治理边界。
什么叫"直通执行"?就是模型说什么就是什么,没有工具注册、没有 trace、没有失败收口、没有状态管理。这在 demo 里没问题,但一旦想把它做成可维护、可调试、可演进的东西,就彻底撑不住了。
所以我给自己定了一个目标:把它改成一个清楚知道自己在干嘛的 Agent 系统。
第一步:把"聊天"改成"Turn"
最先做的事,是引入 Turn(执行单元) 的概念。
原来的系统没有 Turn,只有 Message。每次用户发消息,就是追加一条记录,完全是流水账。
问题在哪?你不知道这条消息是否被处理过、处理到哪一步了、有没有失败。如果中途出错,你只能看日志猜。
我引入 Turn 之后,每一次 Agent 请求都变成了一个有生命周期的执行单元:
STARTED → PROCESSING → SUCCESS / DEGRADED / FAILED
Turn 有租约控制,同一个会话不能并发跑两个 Turn。这一个改动,把"调模型"变成了"执行一次可审计的 Agent 决策"。
感觉是质变,不是量变。
第二步:Trace 让 Agent 行为变得"可看"
Turn 有了之后,下一个问题:Agent 到底在干什么,我怎么知道?
之前的答案是:看日志。问题是日志是流式输出,没有结构,找个问题要翻半天。
所以我做了 Trace 系统。每一步 Agent 行为------输入 guardrail 校验、决策生成、工具调用开始/结束、委派、审批等待------都会落一条 Step Trace,带 status、startTime、durationMs、toolOutput。
java
// 工具执行前:开始 trace
traceService.startToolStep(turnId, toolName, toolInput);
// 工具执行后:记录结果
traceService.completeToolStep(turnId, toolOutput);
前端有个 Agent Workbench,可以看到每个 Turn 的完整执行链路,哪步花了多久、工具返回了什么、是否触发了 guardrail。
这个东西做完之后,我第一次觉得:"这才是 Agent,不是聊天页。"
第三步:工具调用 + 统一输出归一化
Agent 真正有用,是因为它能调工具。我给系统注册了两个只读工具:
read_resume:读取用户简历画像search_knowledge_base:检索知识库(走 pgvector + RAG 链路)
但光有工具还不够,工具的输出格式必须统一,否则 Prompt 组装、Memory 写回和 Trace 记录都要分别处理,维护成本会炸。
我设计了 AgentToolResult 这个统一视图:
java
// 每个工具都输出这个结构
AgentToolResult {
summary, // 给 Prompt 的一句话摘要
answerPayload, // 结构化答案(给 LLM 用的上下文)
debugPayload, // 调试信息(不进 Prompt,只给工作台)
confirmedFacts // 可信事实(写入 Memory)
}
这个设计的好处是:不管是简历工具还是知识库工具,输出进来之后,Prompt 组装、Memory 更新、Trace 记录走的是同一条路,不需要为每个工具写专属逻辑。
后来 handoff(受控委派)也直接输出 AgentToolResult,workbench 不需要为委派单独发明一套展示协议。这个决定省了很多事。
第四步:Guardrail 和 Approval------让 Agent 有约束
这是我觉得最有意思的一个设计决定。
很多 AI 项目在"安全"上的做法是:事后判断输出是否有问题。但这个思路有个根本问题------工具如果有副作用,你执行完再发现不合适,已经晚了。
所以我的 Guardrail 和 Approval 都发生在执行之前:
- Input Guardrail:用户输入先过一遍校验,不符合的直接拦截,不进入 Agent 决策链路
- Tool Guardrail :高风险工具调用,
resolveDecision()会标记需要人工审批 - Approval:真正的工具执行挂起,等待审批通过后才继续
审批通过后的恢复逻辑我设计了三种语义:
| 恢复路径 | 场景 |
|---|---|
EXECUTE_TOOL |
trace 还在 WAITING_APPROVAL,工具尚未执行,批准后可以安全执行 |
FINALIZE_FROM_TRACE |
工具已执行完毕,直接从 trace 恢复结果,不重跑 |
BLOCK_REPLAY |
trace 状态不明,为防止副作用重复,显式阻断并降级 |
这三条路走下来,审批恢复的正确率 100%,没有出现过"重复执行同一个工具"的情况。
第五步:Memory,真正减少重复劳动的地方
这是做完之后最有成就感的一个模块。
Agent 多轮对话最大的问题不是"记不住",而是重复读取已知信息 。第一轮读了一次简历,第二轮还会再去 read_resume 一次,因为 Prompt 里没有记住"我已经知道这份简历的关键信息了"。
我用结构化 Memory 解决了这个问题:
memory {
phase: INTERVIEWING,
confirmedFacts: ["候选人熟悉 Spring Boot", "有 Redis 使用经验"],
nextFocus: "深入追问分布式锁的使用场景",
usedTools: ["read_resume"]
}
Memory 在每次工具调用后写回,下次 Turn 装配上下文时先读 Memory,usedTools 里有的就不再重复调用。
量化结果:在 9 组固定 Memory 样本测试中,重复工具读取从 3 次降到 0,重复事实确认从 2 次降到 0。
这让 Agent 在多轮任务里的行为开始变得像"有记忆的人",而不是"每次都从零开始的机器"。
第六步:多步执行 + 预算控制
单步 Agent 够用,但有些任务需要多步决策------检索知识库,再基于结果出追问题,再评估答案。
我加了受控多步 loop,但默认不开,需要显式传 runtimeConfig.multiStepEnabled=true 才会进入。
多步执行有三类预算:
maxSteps:最多执行几步maxDurationMillis:最多跑多少毫秒maxEstimatedModelTokens:预估 Token 上限
任意一个触发,就进入降级收口,不继续跑。terminalState 统一给 UI 和 metrics 提供语义,不是简单的"成功/失败"。
踩坑集锦
做这个项目让我对"Agent"的理解从"调大模型"变成了"工程化编排",但过程里也踩了不少坑:
坑1:pgvector 向量表不自动创建
initialize-schema: false 时 Spring AI 不创建 vector_store 表,启动就报错。开发环境记得设 true。
坑2:JPA ddl-auto 第一次用 create,以后必须换成 update
第一次启动 create 没问题,下次重启就把所有数据库表删掉重建了......把测试数据全搞没了,痛一次就记住了。
坑3:Redis Stream Consumer 不够健壮
简历分析任务失败后,Consumer 没有正确 ACK,导致同一条消息被重复消费了 3 次。后来统一加了重试计数和死信队列逻辑。
坑4:SSE 不能走 axios
知识库问答的流式响应走 SSE,结果发现 axios 的拦截器会等全部数据到了再 resolve,流式输出就没了效果。前端改成直接 fetch 读 ReadableStream。
写在最后
这个项目做到现在,我觉得最大的收获不是某个具体技术,而是对"Agent 工程化"有了真实感受。
大模型调 API 很容易,但 "让 Agent 行为可解释、可约束、可调试、可演进",才是真正难的地方。
Turn、Trace、Memory、Guardrail、Approval------每一个模块单独看都不复杂,但它们组合在一起,才让 Agent 从一个"聊天页"变成了一个有执行状态、有安全边界、可以持续迭代的系统。
如果你也在做 Java 后端方向,想找一个把 Spring AI 用深的实践项目,这个方向真的值得搞。
代码在 GitHub:[Kiyra-gjx/interview-guide](