Java 老兵一周手撕 LLM:我们到底在封装什么
一个 8 年 Java 后端,用 OkHttp + Jackson 徒手撸完 LLM / Function Calling / ReAct / RAG 全链路的一周复盘。
不装框架,不用 SDK,就想搞清楚一件事——所有那些"AI Agent 框架"到底在封装什么。
缘起:从"这玩意黑盒吗"开始
作为 Java 后端,看 LangChain / Spring AI 的教程时总有一种熟悉又陌生的感觉——熟悉的是"依赖注入 + 责任链"这套味道,陌生的是它下面到底在跟 LLM 聊什么。
于是决定:框架先扔一边,用 OkHttp + Jackson 从 HTTP 层撸一遍。就像当年学 Spring MVC 前先手写 Servlet 一样。
一周,5 个 Main 类,全部提交 GitHub。这篇是复盘。
技术选型:Java 17 + OkHttp + Jackson,LLM 用火山方舟豆包(OpenAI 兼容接口,国内稳)。
Day 1:LLM API 就是一次 HTTP POST
第一天最大的认知冲击不是"LLM 多神奇",而是:它就是一个 HTTP 接口。
POST /chat/completions
Header: Authorization: Bearer <key>
Body: {
"model": "doubao-seed-1-6",
"messages": [{"role": "user", "content": "你好"}]
}没有 SDK,没有中间件,没有任何"AI 专用协议"。就是 REST。
行业事实标准:OpenAI 的 /chat/completions 格式已经被火山、DeepSeek、通义千问、Ollama 全部兼容。换厂商只改 baseUrl 和 model,代码零改动。这个认知直接决定了后面所有事——LLM 应用的下层就是 HTTP 客户端。
Day 2:LLM 是无状态的,"记忆"是客户端拼出来的
第二天颠覆的认知:LLM 每次调用是完全独立的,模型自己不记得上一轮聊了啥。
所谓"多轮对话",是客户端把历史消息数组每次都发一遍:
第 1 轮:messages = [user1]
第 2 轮:messages = [user1, assistant1, user2]
第 3 轮:messages = [user1, assistant1, user2, assistant2, user3]推论直接就来了:
- 对话越长,token 越贵(历史每轮重发一遍)
- 长对话必须做上下文压缩 / 滑动窗口,否则撞窗口上限
- 所有"Memory / ChatMemory"框架的核心,就是管理这个
List<Message>
这一刻,Spring AI 的 ChatMemory 我已经能猜到接口长啥样了——一个存 message、一个取 message、一个截断策略。
顺手把流式也撸了:"stream": true + SSE 逐行读 data: ...,OkHttp 用 EventSource 优雅,手写也就 20 行。打字机效果不是模型的能力,是网络传输方式。
Day 3:Function Calling —— Agent 的心脏
这是全周最关键的一天。面试送分题:tool_calls 到底谁调用?
正确答案:你的代码。LLM 只是返回一个"我想调 XX 函数,参数是 YY"的信号(JSON 格式),实际执行完全在客户端。
完整链路 5 步:
- 客户端把 tool schemas 塞进请求("你有这些工具可以用")
- LLM 分析用户问题,返回
tool_calls: [{name, arguments}] - 客户端执行函数(这一步 LLM 完全不参与)
- 客户端把结果作为
role=tool消息追加到 messages,再发一轮 - LLM 拿到结果,生成人类可读的回复
写完 ToolCall.java 那一刻通了很多事:
- 所谓"AI 帮你查天气",AI 只是决定要查,查还是你查
- 所谓"AI 帮你发邮件",AI 只是决定要发,发还是你发
- AI 是决策器,不是执行器
一个 Java 后端看到这一步的第一反应应该是——这不就是策略模式 + 责任链? 是的,Spring AI 的 @Tool 注解,本质就是把这套 5 步流水线封装成注解,schema 自动生成、函数自动匹配、结果自动回喂。
你现在知道它下面在干什么了。
Day 4:ReAct 循环 —— Agent 就是一个 while
Day 4 写 ReAct Agent,我原本以为会很复杂——毕竟"AI Agent"这词被吹得神乎其神。
结果代码骨架是这样:
while (!done) {
Response resp = llm.chat(messages);
if (isFinalAnswer(resp)) break;
if (isToolCall(resp)) {
String result = executeTool(resp.function, resp.args);
messages.add(toolMessage(result));
}
if (++round > MAX_ROUND) break;
}就这。
- LangChain 的
AgentExecutor= 这个 while + 工具注册 + 错误处理 - LangGraph = 这个 while + 图结构 + 状态管理
- Spring AI 的
ChatClient= 这个 while + 切面 + 自动序列化
Prompt 版 ReAct 用 System prompt 教模型输出 Thought / Action / Observation 三段格式,然后正则解析。跑一个 case:"北京比上海人口多多少万?"——模型自己规划两次 search(分别查两城人口)+ 一次 calculator(做减法),4 步收敛。
看清楚整个循环打印出来的那一刻,你就理解了所有 Agent 框架的"魔法"在哪个文件里。
Day 5:RAG 的本质是"检索什么塞进 Prompt"
RAG 现在被讲得像"必须用向量数据库"的样子。其实不是。
RAG 三件套:
- 检索什么 —— 从语料里选出与 query 最相关的片段
- 怎么拼 —— 把片段组织进 system / user prompt
- Prompt 怎么约束 —— 明确告诉 LLM "资料未提及请说不知道",防止幻觉
Day 5 我故意不用向量库——用最土的 Jaccard 相似度(词集交并比)算相关性,Top-2 塞进 prompt。
结果?简单查询完全够用。幻觉不在生成层,在检索层——检索错了,生成再严谨也是"基于错误资料严谨地编"。
Jaccard 的问题在哪:
- 无法处理同义词("报酬" vs "薪资")
- 无法理解上下文("苹果" 是水果还是公司?)
这就是为什么下周(Week 2)要上 Qdrant + Embedding 做语义检索——但要意识到,向量库只是把"检索什么"这一步做得更聪明,RAG 的整体骨架没变。
Day 6:Prompt 工程的天花板不是技巧,是任务定义
Day 6 做用户反馈三分类(bug / 咨询 / 建议),三版 Prompt 对比:
- v1 Zero-shot:只描述任务
- v2 Few-shot:加 5 个跨类别示例
- v3 CoT:Few-shot + "请一步一步分析" + 每类特征定义
结果:
- 简单题:三版全 10/10 = 100%,Prompt 工程没影响
- 脏样本(反问、混合意图、隐式抱怨):三版全 8/10 = 80%,技巧升级完全没用
深挖发现——三版错的都是同两条:一条是"我用了一小时才搞明白"(该分 bug 还是建议?边界本身就模糊),一条是"能不能加个夜间模式"(是建议还是咨询?定义就没约束死)。
结论:Prompt 工程的天花板是任务定义本身,不是技巧不够多。CoT 让模型讲得更漂亮,但改不了它对"模糊边界"的困惑。
沉淀了一套五步法:任务定义 → Few-shot 覆盖边界 → CoT 打开思路 → 输出格式约束 → 迭代复盘。
我们到底在封装什么?
一周下来,回头看那些 AI Agent 框架,答案清晰了:
| 框架封装的东西 | 底下真实的样子 |
|---|---|
| ChatClient / LLM SDK | HTTP POST + JSON |
| ChatMemory | List<Message> + 滑动窗口 |
| @Tool 注解 | Schema 生成 + tool_calls 解析 + 函数分发 |
| Agent Executor | while (!done) { chat(); dispatch(); } |
| VectorStore | 相关性打分 + Top-K + Prompt 拼接 |
| Advisor Chain | 请求切面 / 责任链 |
没有一个是新东西。都是 Java 后端十多年前就在用的模式——HTTP 客户端、集合操作、策略模式、责任链、AOP。
变的只是上游变成了一个概率性 API——它不像数据库那样保证"输入 A 出 B",它会"输入 A 大概率出 B,偶尔出 C"。所以框架多做的一件事,是在不确定性上加护栏:ChatMemory 防遗忘、Tool Schema 防幻觉、ReAct 循环防跑偏、RAG 防编造。
这就是 AI 应用工程化的本质。
给同样是 Java 老兵的建议
如果你也想入 AI Agent 这个坑,我的建议是——先手撕,再上框架。
一周时间不长。5 个 Main 类,600 行代码,一个 OpenAI 兼容 key(火山方舟便宜、稳、国内网络无坑),你就能把这一层的黑盒撕开。
之后再看 Spring AI / LangChain4j,你不会再有"这个魔法怎么变的"的疑问——你只会说"哦,这一段就是我 Day 3 那 30 行 tool_calls 处理","这一段就是我 Day 4 的 while 循环"。
框架是省你时间的,不是省你理解的。 理解不能被框架封装。
后续计划
- Week 2:Spring AI + Qdrant,做一个真正的简历问答机器人(v1)
- Week 3:LangGraph4j / Spring AI Agent,多智能体协作
- Week 4-:MCP、生产化部署、评测体系
GitHub 项目:agent-learning-week1 姊妹篇预告:《手撕 vs Spring AI:从 Servlet 到 SpringMVC 的顿悟》(Week 2 复盘)
一个 Java 老兵,写于 2026-07-19