news 2026/9/13 3:22:48

Java+DDD复刻Deepseek Harness:大模型工具调用与Agent编排实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java+DDD复刻Deepseek Harness:大模型工具调用与Agent编排实践

先从结论说起:我花了两周时间,用 Java 21 + Spring Boot 3 + DDD 领域驱动设计,把社区里那个很火的 Deepseek Harness 项目按 1:1 的思路重新实现了一遍。这里说的“1:1”,不是逐行翻译源码,而是把它的核心机制——大模型工具调用(Function Calling)的封装层、Agent 编排链路、上下文管理策略——用 DDD 的方式重新拆解、建模、落地。整个过程踩了不少坑,也把很多“知其然不知其所以然”的细节彻底搞懂了。

这个项目适合谁?两类人。第一类是想搞清楚 Harness 和 Agent 到底差在哪、大模型工具调用链路内部是怎么跑的 Java 后端工程师;第二类是正在学 DDD,想知道领域建模在实际项目中怎么落地、而不是只会写“订单-商品-用户”教学案例的人。如果你两个点都占,那这篇文章基本就是给你写的。我会把事件风暴怎么开、限界上下文怎么切、工具注册表怎么设计、Deepseek API 怎么接、上下文超限怎么处理,全部按照我实际动手的过程讲一遍。

1. 为什么要用 Java + DDD 复刻一个 Harness

1.1 Harness 到底是个什么东西

很多同学第一次看到 Harness 这个词是在 AI Agent 相关的仓库里,比如 Codex Harness、Deepseek Harness、Agent Harness。听着很唬人,其实它的核心作用就一句话:在模型和应用之间加一层“缰绳”。你看英文里 harness 本身就有“马具、挽具”的意思,套在 AI 上就是控制模型行为的工具层。

拆开看,一个典型的 Harness 要做四件事:

  • 工具注册与发现:告诉模型“你现在有哪些工具可以用”,比如查天气、算数、查数据库、调外部 API。
  • 工具调用的请求与响应编排:模型输出一个结构化的“我想调用某某工具,参数是某某”,Harness 负责解析、执行、把结果回传给模型。
  • 对话上下文管理:维护完整的消息历史,并在超长时做裁剪或摘要,保证多轮工具调用不迷路。
  • 安全与边界控制:哪些工具能调、哪些不能调、调用超时怎么处理、并发怎么控制。

如果你用过 ChatGPT 的插件功能,或者 Deepseek 的 Function Calling,那你已经在用 Harness 的思路了,只不过那些是平台帮你封装好的。自己用 Java 复刻一遍,相当于把黑盒拆开,看里面到底是怎么转的。

1.2 为什么选 Java,而不是 Python

我知道你肯定想问:AI 生态不都是 Python 的天下吗?用 Java 复刻一个 AI 框架,是不是有点逆潮流?

我当时的判断是反过来的。第一,团队现有技术栈就是 Java,Spring Boot 的生态成熟,部署运维、监控告警、灰度发布这一套都是现成的,为了一个几十万 token 的调用链路单独引入 Python 服务,成本不划算。第二,真实的企业级 AI 应用很少是纯模型调用,它一定要和现有的业务系统打交道——订单系统、库存系统、权限系统,这些在 Java 世界里已经沉淀了大量领域逻辑。与其用 Python 做一层薄薄的胶水层,不如直接在 Java 里把 Harness 做成基础设施。

第三点可能更现实一点:Java 面试里,AI 应用开发的经验正在变成高价值加分项。你去看现在的岗位要求,尤其是一些中大型互联网公司的 Java 岗位,已经在要求“熟悉大模型应用开发,了解 Function Calling、Agent 编排”。而大多数人还停留在“用 Python 调一下官方 SDK demo”的水平。如果你能用 Java + DDD 做出一套结构清晰、可扩展的 Harness,这在面试聊起来是完全不同的深度。

1.3 DDD 在 AI 项目里能发挥什么价值

说实话,DDD(领域驱动设计)这两年有点被妖魔化了。一提 DDD 就是事件风暴、聚合根、领域事件、CQRS,一套组合拳下来,很多人只记住了术语,落到代码里还是 CRUD。

但在这个 Harness 项目里,DDD 的价值非常实在。因为 Harness 本身的业务复杂度并不低:既要有模型接入、工具注册这类“技术域”,又要有会话管理、消息流转这类“业务域”,还有上下文策略、Token 计价这类杂糅了业务和技术的部分。如果不用领域模型把这些边界切清楚,写到最后一定是一团互相引用的意大利面。

DDD 的核心动作是识别限界上下文、建立通用语言、划分聚合边界。这几个动作做完,你会发现一个 AI Harness 项目天然就是多个限界上下文的组合:模型接入上下文、工具管理上下文、会话编排上下文、执行引擎上下文。每个上下文内部是自治的,上下文之间通过明确的接口通信。这不只是写着舒服,更是为了后续演进——今天接的是 Deepseek,明天要接通义千问或者本地部署的模型,只要模型接入上下文的适配层做得干净,替换成本会非常低。

2. 领域建模:先用事件风暴把业务拆清楚

2.1 事件风暴该怎么组织

动代码之前,我们拉了一个下午的事件风暴(Event Storming)工作坊。别被这个词吓到,实际操作就是把业务方、后端、测试拉到一起,用便利贴在墙上贴“领域事件”,然后倒推触发这些事件的“命令”,再找承载状态的“聚合”。

对于 Harness 项目,我们列出来的核心领域事件包括:

  • ToolRegistered(工具已注册)
  • ToolInvocationRequested(工具调用已请求)
  • ToolInvocationSucceeded(工具调用成功)
  • ToolInvocationFailed(工具调用失败)
  • MessageAppended(消息已追加)
  • ContextTruncated(上下文已裁剪)
  • SessionEnded(会话已结束)

每个事件旁边,贴上是谁触发的。比如 ToolInvocationRequested,触发的命令来自模型输出的 tool_calls 字段;ToolInvocationSucceeded,触发的命令来自工具执行器的返回值。这样一贴,整个系统的动态流程就出来了,比看十遍架构图都直观。

2.2 限界上下文划分:五个边界清晰的模型域

事件风暴做完,我们把系统切成了五个限界上下文。这是整个 DDD 设计中最重要的决策,直接决定后续代码结构长什么样。

限界上下文核心职责关键领域对象
模型接入上下文(Model Access)封装不同大模型 API 的差异,统一调用入口ModelClient、ChatRequest、ChatResponse
工具管理上下文(Tool Management)工具的注册、发现、参数 Schema 管理ToolRegistry、ToolDefinition、ToolSpec
会话编排上下文(Session Orchestration)会话生命周期、消息历史维护、上下文策略ChatSession、MessageHistory、ContextStrategy
工具执行上下文(Tool Execution)调起真实的工具逻辑、处理超时与异常ToolInvoker、InvocationResult、ToolException
应用服务上下文(Application)对外提供 API,串联上面四个上下文HarnessApplicationService、Facade

这里有个常见的坑:很多人会把“工具执行”和“工具管理”合并成一个上下文。我建议拆开。原因很简单,工具管理关心的是“有哪些工具、长什么样”,工具执行关心的是“怎么跑起来、出错了怎么处理”。两者变化的频率和原因完全不同。比如你给工具管理加一个注解扫描的新特性,不该影响到执行器那部分代码。拆开后,各自的聚合边界也更清晰。

2.3 聚合与实体设计:别把聚合根做成大泥球

DDD 落地时最容易犯的错误,是把聚合根做成一个大而全的对象,什么字段都往里塞。我们这个项目里最重要的聚合根是 ChatSession,它的设计就经历了从“大泥球”到“瘦身”的过程。

第一版我把 ToolRegistry、MessageHistory、ContextStrategy 全部塞进 ChatSession,字段有几十个。结果发现一个会话既要做消息追加,又要管工具注册索引,还要处理上下文裁剪,任何一个小的变更都会牵动整个聚合,测试也很难写。后来按 DDD 的原则重新梳理:ChatSession 聚合根只维护最核心的不变条件,包括会话 ID、关联的 ModelClient、消息列表,以及当前会话的 Token 占用情况。工具注册不放在会话里,因为工具是全局共享的;上下文裁剪策略也不直接挂在会话实体上,而是通过策略对象传入。

聚合内的一致性通过聚合根统一对外提供方法保证。比如追加消息时,ChatSession 内部会自己判断当前 Token 占用是否超过阈值,如果超了就先触发 ContextStrategy 执行裁剪,再追加新消息。这个“先裁剪再追加”的逻辑必须由聚合根保证,不能让应用服务层来做,否则以后换个调用入口就可能漏掉这个约束。

2.4 通用语言落地:团队先对齐名词再说代码

DDD 强调通用语言(Ubiquitous Language),目的是让业务人员和开发人员说同一套词。我们这个项目里最典型的例子就是“工具”这个词的混乱。

一开始团队里有人说“工具”,有人说“插件”,有人说“Skill”,还有人说“Action”。代码里同时出现 Tool、Plugin、Skill 三个类名,指向的却是同一个东西。后来我们花了一个小时统一术语表:

统一术语含义废弃说法
Tool一个可被模型调用的功能单元插件、Skill、Action
ToolCall模型发起的一次具体工具调用请求Function Call、工具调用记录
ToolSpec工具的元数据和参数 Schema 定义工具描述、OpenAPI 定义
Harness整套模型-工具编排引擎Agent 框架、插件系统
Session一次多轮对话的上下文载体对话、聊天记录

术语统一之后,代码命名、数据库表名、API 字段名全部跟着改。这个动作看起来不产生任何业务功能,但后面写代码时效率至少提升 30%,因为大家不用再互相问“你这个 plugin 指的是哪种 plugin”。面试时如果聊到 DDD,这个例子也很能说明你对通用语言的理解不是停留在概念层面。

3. 核心链路实现:从工具注册到 Deepseek 调用

3.1 分层架构与工程目录

限界上下文确定后,代码结构就顺理成章了。我采用的是经典的 DDD 分层:接口层(interfaces)→ 应用层(application)→ 领域层(domain)→ 基础设施层(infrastructure)。

com.example.ds-harness ├── interfaces — Controller、DTO、请求校验 ├── application — 应用服务、DTO 转换、事务编排 ├── domain │ ├── model — 模型接入上下文 │ ├── tool — 工具管理上下文 + 工具执行上下文 │ ├── session — 会话编排上下文 │ └── shared — 通用值对象、领域事件 └── infrastructure ├── client — DeepseekApiClient、OkHttp/WebClient 封装 ├── repository — Redis 实现、JPA 实现 └── config — 配置项、Bean 装配

这里有个实操体会:domain 包下面不要再按“实体、值对象、仓库接口、领域服务”这种技术分类建子包,而是按业务上下文建包。比如 session 上下文里,自然会有 ChatSession 实体、Message 值对象、ContextStrategy 接口、ChatSessionRepository 接口。这样你一眼就能看出“这个上下文有哪些领域概念”,而不是还得一个包一个包点开看。不少人学了 DDD 但代码看着还是像三层架构,就是因为包的切法根本没按领域来。

3.2 工具注册与发现:让模型知道“你能干什么”

工具注册是整个 Harness 的基础。模型本身不知道你的系统有哪些能力,你得在请求里带上工具清单。我们通过自定义注解 + 注册表的方式实现。

核心是一个 @HarnessTool 注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface HarnessTool { String name(); String description(); String[] parameterNames() default {}; String[] parameterDescriptions() default {}; boolean required() default true; }

然后定义一个 ToolRegistry 接口,属于工具管理上下文的领域层:

public interface ToolRegistry { void register(ToolDefinition definition); Optional<ToolDefinition> find(String name); List<ToolDefinition> findAll(); }

基础设施层提供一个基于 Spring 容器扫描的实现,启动的时候把带有 @HarnessTool 注解的方法自动注册进去。但这里有个最关键的细节:工具的注册信息最终要转换成大模型能识别的 JSON Schema 格式。Deepseek 的 Function Calling 接口遵循 OpenAI 的格式,每个工具需要提供 type、function、parameters 三件套。

我自己封装了一个转换器,把注解里的参数名和描述转成 JSON Schema:

public class ToolSpecGenerator { public Map<String, Object> generateSpec(ToolDefinition def) { Map<String, Object> parameters = new HashMap<>(); parameters.put("type", "object"); List<Map<String, Object>> properties = def.getParameters().stream() .map(p -> { Map<String, Object> prop = new HashMap<>(); prop.put("type", "string"); prop.put("description", p.getDescription()); return prop; }).collect(Collectors.toList()); parameters.put("properties", properties); Map<String, Object> function = new HashMap<>(); function.put("name", def.getName()); function.put("description", def.getDescription()); function.put("parameters", parameters); Map<String, Object> spec = new HashMap<>(); spec.put("type", "function"); spec.put("function", function); return spec; } }

这个转换看起来简单,但有一个特别容易被坑的地方:如果参数没有声明 enum 或者 format,某些模型会把所有参数都当字符串处理,导致传数字类型的参数时模型给出一个字符串。后面第 4 节我会专门讲这个问题。

3.3 工具调用编排:一次完整的 ReAct 循环

工具调用的编排是整个 Harness 的核心链路。我把它理解成一个带终止条件的 while 循环,每一步做五件事:

  1. 把系统提示词、历史消息、工具清单组装成请求。
  2. 调用 Deepseek API,拿到模型响应。
  3. 判断响应里有没有 tool_calls。没有,说明模型觉得任务完成了,直接返回给用户。
  4. 有 tool_calls,遍历每一个调用,去 ToolRegistry 找到对应工具,执行。
  5. 把每个工具的执行结果以 “tool” 角色的消息追加回会话,再次组装请求调用模型。

这个循环的经典称呼是 ReAct(Reasoning + Acting),也就是“思考-行动-观察”的循环。模型先思考,决定要调什么工具,系统执行工具,把结果返回给模型“观察”,模型再继续思考。直到模型觉得不需要工具了,输出最终答案。

我建议最大循环次数限制在 5-8 轮,防止模型陷入无限调用。比如模型反复调用一个成功的工具,把结果回传后又调用同一个工具,这种循环在真实场景里非常常见。我们的做法是在应用服务层设置 maxIterations 参数,默认 5,超出后直接抛出 MaxIterationExceededException,把当前的消息历史原样返回给前端,让前端提示用户“当前任务太复杂,请拆分成多个子任务”。

3.4 Deepseek API 接入细节:兼容 OpenAI 格式的 Client

Deepseek 的 API 设计成了兼容 OpenAI 格式,所以调用方式非常直接。我们用 Spring 的 WebClient 封装了一个基础设施层的 ModelClient 实现。先定义领域层接口:

public interface ModelClient { ChatResponse chat(ChatRequest request); Flux<ChatResponse> chatStream(ChatRequest request); }

基础设施层实现 DeepseekModelClient,核心就是拼这个 JSON 请求体:

{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是 Harness 引擎的助手。"}, {"role": "user", "content": "帮我查一下北京今天的天气"} ], "tools": [ { "type": "function", "function": { "name": "weather_query", "description": "查询城市天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } } } ], "tool_choice": "auto" }

技术细节上,我用的 WebClient 响应超时设置为 60 秒,连接超时 10 秒。流式接口在低延迟场景下更好用,但会显著增加代码复杂度,因为 SSE(Server-Sent Events)流的解析、工具调用结果的流式回传都要处理。我的建议是初期先做非流式,把链路跑通,再考虑流式优化。接入配置放在 application.yml 里,通过 @ConfigurationProperties 绑定到 DeepseekProperties:

deepseek: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com model: deepseek-chat temperature: 0.7 max-tokens: 4096

有个非常容易忽略的点:tool_choice参数。默认值是auto,意思是模型自己决定要不要调工具。但在某些场景下你需要强制模型调用某个工具,比如用户说“把这段文本翻译成英文”,而你只有一个translate工具,这时候可以把tool_choice设置为{"type": "function", "function": {"name": "translate"}},模型就不会跑偏去自己瞎翻译了。这一点在我实际测试中对稳定性的提升非常明显。

3.5 上下文管理:会话不能无限膨胀

聊过 Deepseek 的人应该都遇到过“对话长度达到上限,请开启新对话”的提示。这个问题的本质是:模型有上下文窗口限制,Deepseek-V3 的上下文窗口大约 64K token,超了就得清。但清掉了前面的信息,多轮工具调用就会断掉,比如模型前面说过“我接下来会查库存”,后面突然被截断,它就不记得了。

我设计了分层的上下文管理策略,核心接口是 ContextStrategy:

public interface ContextStrategy { List<Message> compact(List<Message> messages, int maxTokens); }

实现类至少需要这三种策略:

  • 滑动窗口裁剪(SlidingWindowTruncationStrategy):只保留最近的 N 条消息。优点是实现简单、速度快;缺点是中间过程全部丢失,多轮工具调用的链路一旦被截断,后续就可能出现上下文不一致。
  • 摘要压缩(SummarizationContextStrategy):调用模型,把前面的对话总结成一段摘要,作为 system 消息填充。保留信息完整性更好,但多了一次模型调用,增加延迟和成本。
  • 关键信息保留(KeyInfoPreservationStrategy):从工具调用结果中提取关键字段保留。这是最轻量、最高性价比的方案,适合工具调用结果很大、但诊断信息并不需要的场景。

我实际采用的组合策略是:优先做关键信息保留,把上一轮的 ToolCall 和 ToolResult 精简成一行摘要,比如[工具: query_weather, 参数: {"city": "北京"}, 结果: 晴, 25°C]。如果消息数仍然超过阈值,再走摘要压缩。滑动窗口作为最后的保底方案。这一套做完,连续 20 轮工具调用的场景基本不会触顶。

4. 实战踩坑与排查记录

4.1 报错一:Deepseek 一直提示“达到对话长度上限,请开启新对话”

这个错误几乎每个接 Deepseek 的人都会遇到。我一开始以为是没做上下文裁剪,后来排查才发现更隐蔽的问题:工具调用结果里有冗余的原始响应结构。我在把 ToolResult 追加回消息时,直接放入了整个 JSON 响应体,包括 HTTP 状态码、响应头、调用链路的 traceId 等等。一次工具调用无害,但 5 轮工具调用后,这些元信息可能占掉上万 token。

解决方案很简单:定义 Message 值对象时,只保留 role、content、toolCallId、name 这几个核心字段,工具原始响应只保留 data 部分,其他全部丢弃。同时设置单个 ToolResult 的最大长度,超过 2000 字自动截断。这样做之后,同场景下 token 占用少了 60% 以上,连续多轮调用也再没触顶。

4.2 报错二:模型的 tool_calls 参数经常解析失败

另一个高频问题是:模型返回的 tool_calls 里的参数是个 JSON 字符串,而不是结构化的 JSON 对象。比如模型可能返回:

{ "tool_calls": [{ "function": { "name": "weather_query", "arguments": "{\"city\":\"北京\"}" } }] }

注意 arguments 字段是字符串。如果你直接拿这个字符串去做toolCall.getArguments().get("city"),一定报错或者拿到 null。正确做法是先反序列化成 JsonNode 再取值,同时要做异常兜底,因为模型偶尔会生成非法的 JSON,比如多个 JSON 拼接。

我写了一个安全的工具调用参数解析器,做了三重兜底:先尝试正常反序列化;失败后用容错 JSON 解析库(比如 json-sanitizer)清洗后再次解析;还失败就返回一个空的参数 Map,并附带一条 warning 消息回传给模型,让它知道参数解析失败。这个兜底逻辑在实际运行中救了很多次。

4.3 报错三:工具参数的 JSON Schema 类型不对,模型把数字当了字符串

有一次在本地调试,我注册了一个入参为int类型的工具,但没在 @HarnessTool 注解里声明参数类型。结果模型连续三次调用都传入"25",而工具的 Java 方法签名明明是int。我一开始以为是类型没问题,后来才发现是 ToolSpecGenerator 里默认把所有参数都标成了string。Deepseek 对 Schema 类型非常敏感,你标了 string,模型就会老老实实传字符串。

修复很简单:在注解里增加参数类型声明,支持stringnumberintegerbooleanobject五类,并在转换时做映射。另外还要处理enum情况:如果某个参数只允许几个枚举值,最好在 Schema 里显式声明enum列表,否则模型可能自由发挥。

4.4 并发场景:Redis 自增计数和幂等控制

如果你把 Harness 部署成多实例服务,会话状态就不能只存在本地内存里。我用 Redis 存储会话上下文和工具调用计数。这里遇到一个很实际的问题:用 Redis 的increment()做工具调用次数限制时,第一次会报错“not an integer or out of range”,因为 key 不存在,increment()在有些配置下返回的不是预期的数字,而是把空值当成异常处理了。

排查后确认是 Jedis 和 Spring Data Redis 的版本行为差异。正确用法是先判断 key 是否不存在,不存在就先setIfAbsent(key, "0", Duration.ofMinutes(5))初始化,再increment()。同时注意设置过期时间,因为工具调用的频次限制通常是按时间窗口计算的,比如一分钟最多调用 100 次。我最后封装了一个 RateLimiter 组件,基于 Redis + Lua 脚本保证原子性,避免并发场景下计数错乱。

4.5 DDD 落地中的典型争议与我的取舍

最后说一些 DDD 落地时的真实感受。网上对 DDD 的批评,主要集中在“过度设计”“聚合根边界混乱”“事务跨聚合导致一致性难保证”这几个点。我在这个项目里也踩过。

争议最大的一个问题是事务。DDD 的原则是一个事务只修改一个聚合。但 Harness 的工具执行场景天然要跨上下文:工具执行要记录日志(属于另一个聚合),会话要确认已调用状态,还有可能扣费。完全做到单一聚合事务不现实。我的取舍是:核心会话状态的一致性用本地事务保证,工具调用日志和计费用领域事件做异步解耦。具体地,ChatSession 追加一个 ToolCall 并成功执行后,发布 ToolInvocationSucceededEvent,由监听器异步更新日志聚合和计费聚合。这样既保证了主链路的强一致,又避免了跨聚合的大事务。

另一个争议是贫血模型。我发现自己最初写的领域层实际上还是贫血模型——ChatSession 只有 getter/setter,所有逻辑都在应用服务里。后来我强制自己把“追加消息”“执行裁剪”“判断会话结束”这些行为移到实体内部。改造之后,应用服务的代码量大概减少了 40%,而且测试好写很多,因为核心业务逻辑可以脱离 Spring 容器直接做单元测试。这个转变让我真正理解了“行为归属”在 DDD 里的分量。

4.6 常见问题速查表

现象根本原因解决方案
提示“对话长度上限”工具响应未裁剪,或未做上下文策略只保留 data 字段,单条响应限长,分层压缩策略
tool_calls 解析报错arguments 是 JSON 字符串先反序列化再取值,加容错解析兜底
模型把数字参数传成字符串JSON Schema 未声明参数类型注解增加参数类型映射,优先用 integer/number
上下文历史丢失滑动窗口截断太激进改用关键信息保留 + 摘要压缩组合策略
increment()报 not integerRedis key 不存在或版本差异先 setIfAbsent 初始化,再自增,用 Lua 脚本保证原子性
多轮循环不停缺少最大迭代次数限制设置 maxIterations,超出后抛出异常并返回中间结果
默认把所有参数标为 stringToolSpecGenerator 生成逻辑遗漏补充参数类型映射,支持 enum 和 object

5. 这套项目后续还能怎么扩展

复刻完核心链路之后,我明显感觉到这块内容就像积木,往里加东西非常顺。我自己已经验证过的几个扩展方向可以分享给大家参考。

一个是做多模型适配。因为这个项目是 DDD 分层,ModelClient 接口在领域层,Deepseek 的实现只是基础设施层的一个 Bean。再加一个通义千问或本地模型的实现,只需要在基础设施层新增一个 Client,然后在配置类里根据配置项切换 Bean。限界上下文的优势在这时候体现得最明显——模型接入的改动完全不会碰工具管理或会话编排的代码。

另一个是把 Harness 改造成一个可观测的系统。我现在在会话上下文中埋了 traceId,把每一次工具调用的名称、参数、耗时、token 消耗都通过领域事件发到消息队列,再由一个监控服务做聚合展示。这样做最大的价值是能统计出哪些工具被调得最多、哪一步最耗时、token 消耗集中在哪类任务上。这些数据反过来可以优化上下文压缩策略和工具描述文案——模型返回 tool_calls 的准确率,和工具 description 写得好不好有非常大的关系。

如果你正在准备 Java 面试,这个项目的面试价值也很高。几乎每一个点都能当独立话题展开:DDD 聚合设计聊设计能力,工具注册表的反射实现聊 Java 基础,Redis 自增和事务边界聊并发和数据一致性,WebClient 超时设置聊八股文里的网络编程。而且因为是你自己动手复刻的,聊起来会有真实细节,不是背出来的。

最后再分享一个个人体会。刚开始复刻时,我特别想在代码层面做到“原封不动 1:1”,后来发现比起逐行对齐,真正有价值的是把它的机制理解透之后,用自己擅长的方式重写一遍。Java + DDD 的组合,逼着我把 AI 应用开发里那些“用脚本一把梭”的模糊地带,变成了一个个明确的领域边界和可测试的单元。这个过程比单纯跑通一个 demo 学到的多得多。遇到“对话长度上限”或者“tool_calls 解析失败”的时候,也别慌,你先把链路打印出来,看看消息历史在每一步到底长什么样,问题大多一眼就能看出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 3:17:39

SVM支持向量机原理与实战:从鸢尾花理解决策边界与核函数

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 3:12:46

嵌入式最小硬件系统全解析:从电源时钟到PCB调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华