news 2026/10/22 5:00:04

用Effect搭建多提供商Agent框架:Fold核心设计与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Effect搭建多提供商Agent框架:Fold核心设计与实践

如果你写过真正的 Agent,大概率经历过这种阶段——业务逻辑写起来很快,但模型一换就崩;工具调用稍微复杂一点,整个流程就开始在回调地狱和竞态条件里挣扎;报错倒是能拿到,但想判断"这次该重试还是该换策略"全靠玄学。我们团队在做一个内部 Agent 中台时,决定把底层全部用 Effect 重写,最后沉淀出一个叫 Fold 的框架。这篇文章就是 Fold 的核心设计解读:一个基于 Effect、原生支持多提供商接入和工具调用的 Agent 框架,到底是怎么搭起来的,每一步设计背后是什么考量。适合正在用 TypeScript 写 Agent、或者对 Effect 好奇但不知道能拿来干嘛的开发者。

先交代一下背景。Fold 不是一个"学术玩具",它要解决的是我们真实面对的一类问题:同一套 Agent 逻辑,要跑在多家模型服务商上;同一个工具链,要稳定地处理超时、限流、参数幻觉;同一个会话,要跨请求保持状态。而用普通 Promise 链去做这件事,我们试过了,代码会在第三个版本开始失控。

1. 为什么把 Agent 框架押注在 Effect 上

先说一个反直觉的结论:Agent 框架的核心难点,从来不是"调用大模型"本身。模型调用只是网络请求,真正麻烦的是围绕它的一整套控制流——重试、超时、并发、副作用隔离、失败恢复。这些东西如果散落在业务代码里,项目就会变成一座长满 if-else 的废墟。

1.1 手写 Promise 管道的三个痛点

在 Fold 之前,我们的旧框架是标准的手写 Promise 管道:调模型、解析响应、拿结果去执行工具、把工具结果拼回去再调模型。这个循环看着简单,但跑了几个月就陆续暴露问题。

第一个痛点是重试逻辑侵入业务代码。模型接口不是稳定服务,限流和超时是常态。我们在每个调用点都加了类似的指数退避,结果就是几十个文件里复制粘贴同一段逻辑,某个文件忘了更新参数,线上就出现一堆诡异行为。第二个痛点是错误处理没有结构。用 Promise 的 catch 拿到的永远是 unknown,我必须手写一堆判别函数去猜"这到底是被限流了、超时了还是模型返回格式不合法"。猜对了还好,猜错了就把错误信息直接塞给用户,体验非常差。第三个痛点是组合性。Agent 经常要并行做多件事:同时问多个工具、同时尝试两个推理路径、一个模型调用失败时降级到另一个模型。手写 Promise.all 加上一堆条件分支,很快就变成没人敢改的面条代码。

1.2 Effect 带来的关键能力:组合性、类型安全、可重试

Effect 是 TypeScript 生态里的一个效应系统,核心思想是把"会失败的副作用"建模成一个普通的、可组合的值。它的类型签名是这样的:Effect<成功值类型, 失败值类型, 依赖类型>。这个签名看起来抽象,但落到 Agent 场景里每一条都有对应价值。

最直接的收益是重试和退避变成了声明式操作。我不需要在业务里写 for 循环去重试,只需要给某个 Effect 挂一个 retry 策略,像这样:

import { Effect, Schedule, Duration } from "effect"; const safeCompletion = Effect.retry(createCompletion({ messages }), { schedule: Schedule.exponential("100 millis", 2), // 每次退避翻倍 max: 5, while: (error) => error.name === "RateLimitError" || error.name === "TimeoutError", });

重试策略本身变成了一个值,可以在测试里直接 mock,也可以按请求动态调整。这是 Promise 管道很难做到的。

第二个收益是失败路径被类型系统记住了。写完 Effect 之后,"这个调用可能以 RateLimitError 失败"是写进类型里的,不是靠注释。这样我在写下游编排时,编译器会强迫我处理这些情况,而不是等到线上炸了再补。

第三个收益是并发原语。Effect 里有 Fiber,可以像线程一样轻量地 fork 和 join 多个任务,配合 timeout、race、parallel 这些操作符,Agent 里常见的"同时问三个工具,谁先回来用谁"就变得很干净。

1.3 一个前提:团队有没有必要上 Effect

虽然我在这篇文章里对 Effect 赞美有加,但必须说清楚:它不是银弹。如果你的 Agent 只调一次模型、工具就一两个、对稳定性要求不高,那 Promise 完全够用,不要为了技术新鲜感引入整套效应系统,学习和排查成本是实打实的。

Fold 会押注 Effect,是因为我们的场景同时踩中了三个条件:多提供商接入让错误模型很复杂;工具链数量多且互相组合;Agent 循环是长生命周期的,每一轮都可能失败、中断、续跑。在这些条件下,Effect 的类型安全和控制流能力才真正值回票价。如果你的项目也符合这三条,再往下看就有了参考意义。

2. 多提供商抽象层:把"对话"做成一等公民

Fold 叫"多提供商 Agent 框架",多提供商是整个设计的原点。市面上很多框架对多模型的支持就是"在接口后面塞一堆 if",而 Fold 的做法是:把一次模型对话请求做成一等公民,用 Schema 定义契约,用适配器完成映射。

2.1 用 Schema 定义 Provider 契约,而非手写接口

大多数 TypeScript 项目定义接口用 interface,但 interface 只在编译期生效。模型返回的 JSON 是不可信的,它可能缺字段、多字段、字段类型不对。用一个例子说明:你定义response.choices[0].message.content是 string,模型可能返回 null,也可能返回一个对象。interface 不会帮你拦截,运行时该崩还是崩。

Fold 用的是 @effect/schema 的 Schema 来描述请求和响应。Schema 的好处是:既提供了类型推导,又提供了运行时校验。同一份定义既是 TS 类型,又是校验器。请求长这样:

import { Schema } from "@effect/schema"; const Message = Schema.Struct({ role: Schema.Union(Schema.Literal("system"), Schema.Literal("user"), Schema.Literal("assistant"), Schema.Literal("tool")), content: Schema.String, toolCallId: Schema.optional(Schema.String), }); const CompletionRequest = Schema.Struct({ messages: Schema.Array(Message), temperature: Schema.optional(Schema.Number, { default: () => 0.7 }), maxTokens: Schema.optional(Schema.Number), model: Schema.optional(Schema.String), }); const CompletionResponse = Schema.Struct({ content: Schema.String, finishReason: Schema.optional(Schema.Union(Schema.Literal("stop"), Schema.Literal("tool_call"), Schema.Literal("length"))), toolCalls: Schema.optional(Schema.Array(ToolCall)), });

这个设计的核心价值在Schema.decodeUnknown:模型返回的数据进来,先解码,成功才进业务层,失败就给一个结构化错误。你问"为什么需要这一步",答案很直接——模型输出越自由,框架的边界就应该越严格。把不可信的数据挡在门口,后面的 Agent 循环才能假定数据格式是安全的。

2.2 适配器模式:统一请求、结果和异常

有了一份统一的 Schema 契约之后,剩下的工作就是把每家模型服务商的"方言"翻译成这套契约。Fold 里每个提供商适配器只要实现一个方法:

export interface ChatProvider { readonly complete: (request: CompletionRequest) => Effect<CompletionResponse, ProviderError>; }

注意这里返回的是 Effect,不是 Promise。这意味着 Provider 自己的限流、鉴权、网络调用都是 Effect,可以被框架统一调度。实际实现时,适配器内部做的事情无非是:把CompletionRequest映射成服务商要求的请求体,调用对应的 HTTP 客户端,再把响应映射回CompletionResponse。不同服务商的差异点主要集中在字段名、认证方式、流式协议上。

举两个典型例子。某国内厂商的接口走的是 OpenAI 兼容格式,那么适配器要处理的就只是字段映射和认证头。另一个开源推理服务走的是不同协议,可能还需要处理请求体嵌套结构。这些差异全部被适配器消化掉。上层 Agent 永远只跟统一的CompletionRequest/CompletionResponse打交道,换提供商就是换一个依赖注入的 Provider 实例。

这就是为什么 Fold 要多提供商设计,而不仅仅是"写一个便于切换的接口"。接口只能保证编译期一致,不能保证运行时行为一致。通过 Effect 的依赖注入(Requirements),我们可以把 Provider 作为上下文注入到整个 Agent 调用链中,测试时换 mock,生产时换真实实例,整个过程不需要改 Agent 逻辑一行代码。

2.3 流式输出与取消是怎么在 Effect 里收敛的

多提供商里最麻烦的部分是流式输出。不同服务商的流式协议千奇百怪:有的是 SSE 按 token 推,有的是先推 meta 再推内容,有的在流中间插入工具调用标记。Fold 的策略是:不管上游协议多乱,统一成 Effect 的 Stream 类型。

const streamCompletion = (request: CompletionRequest) => Effect.withTies(run)( // 伪代码,示意类型 Stream.make(ProviderMessage) );

统一成 Stream 之后,框架就获得了两件事。第一,流可以被取消——Agent 循环里决定不再需要这段输出了,直接 interrupt 掉对应的 Fiber,底层 HTTP 流也会关闭。第二,流可以被转换组合——比如把 token 流改成按句读聚合,或者在流里检测到工具调用标记时切换处理模式。

这里想分享一个我们踩过的细节:流式传输的取消和 HTTP 连接释放是两回事。单纯中断 Fiber 不一定能关掉底层的网络连接,必须在适配器层用Effect.ensuring确保流结束后释放客户端连接。否则跑一个长时间运行的 Agent 服务,连接数会悄悄涨上去。

3. 核心 Agent 循环:工具调用、规划与副作用隔离

Provider 抽象解决的是"怎么调用模型",而 Agent 框架真正向内的一层是"怎么组织模型和工具之间的循环"。Fold 的核心是一个递归 Effect,每一轮循环都是可中断、可重试、可观测的。

3.1 把 Agent 循环建模成递归 Effect

最常见的 Agent 实现方式是用 while 循环:条件满足就继续迭代。Fold 没有用 while,而是把它建模成递归 Effect。原因很简单:while 循环里每一步都共享同一个错误上下文,一旦某一步爆炸,很难判断该重跑哪一部分;而递归 Effect 让每一轮循环都拥有独立的错误边界和重试策略。

核心逻辑可以简化成这样的结构:

const runAgentLoop = (messages: Message[]): Effect<AgentResult, AgentError> => Effect.gen(function* () { const response = yield* callProvider(messages); if (response.finishReason === "tool_call" && response.toolCalls) { const results = yield* executeToolCalls(response.toolCalls, Effect.forEachSuccess); const newMessages = appendMessages(messages, response, results); return yield* runAgentLoop(newMessages); // 递归进入下一轮 } return { answer: response.content }; });

每一轮递归都接受上一轮的消息列表,返回一个不含副作用描述的结果。这样做最大的好处是:你可以很容易地在任意一层注入超时、重试、日志。比如"整个任务最多跑 10 轮"就可以写成一个外层包装,而不是在循环体内手写计数器再判断。

执行工具调用用的是Effect.forEachSuccess,它还有一个姊妹函数Effect.forEach允许并发执行。工具之间如果没有依赖关系,完全可以并行:同时查询三个数据库,谁先回来谁先进入下一步,省掉的时间非常可观。

3.2 工具注册表与参数校验

Fold 里每一个工具都是注册表中的一个"值"。工具定义包括四部分:名称、描述、参数 Schema、处理函数。处理函数返回的是 Effect 而不是 Promise,这是有意为之——工具本身可能涉及读文件、访问外部 API、修改状态,这些副作用要显式地处于 Effect 管辖内,才能被统一调度、超时、取消。

const weatherTool = Tool.make({ name: "get_weather", description: "查询指定城市当前的天气情况", parameters: Schema.Struct({ city: Schema.String }), execute: (params) => Effect.tryPromise(() => queryWeatherService(params.city)), });

参数校验是工具链里最容易翻车的一环。模型给出的参数经常缺字段、类型错乱、枚举值不在范围内。Fold 里工具执行前强制做Schema.decodeUnknown,校验失败不会直接崩溃,而是变成一个"参数错误"结果,交还给模型让模型重新组织。这个机制一开始我们没做,后来发现模型确实会持续输出带幻觉的参数,比如把城市名写成"北京市北京市",不校验的话工具层根本不知道该拿它怎么办。

工具结果在传回给模型之前还需要处理一步:序列化压缩。一个查询工具可能返回几十 KB 的 JSON,直接塞进 messages 里,下一轮调用模型时会挤占上下文窗口。Fold 的做法是:工具结果统一转成一个精简的文本摘要,超长字段截断,嵌套结构拍平。这一步对成本影响极大,后面还会展开讲。

3.3 失败重试与退避策略:让幻觉成本可控

Agent 循环里的失败分两层:模型调用失败和工具调用失败。这两层要分开处理。模型调用失败,通常值得重试,因为限流、超时是暂时性的;但工具调用失败要小心——如果工具是因为参数幻觉而失败,盲目重试只会放大成本。

Fold 对工具失败的处理策略是"把错误变成消息返回给模型"。工具执行返回一个结构化结果,要么是{ ok: true, data },要么是{ ok: false, error: "..." },后者被塞回上下文,模型看到错误描述后会自己判断是改参数重试还是换一个工具。这个设计的代价是每轮工具失败都会消耗一次模型调用,但它比"工具一失败就中止整个 Agent 流程"要实用得多,因为模型绝大多数情况下确实能自我纠偏。

模型调用层的重试则交给 Schedule。我们实际用的策略组合是:

  • 铁定不可重试的错误(如参数校验失败、上下文超长)直接短路,不进入重试;
  • 可重试错误用指数退避,基础间隔 100ms,上限 5 次;
  • 整个 Agent 任务外层再包一个总超时,超时即终止,并返回一个带"部分完成状态"的 AgentError,方便上层决定是否续跑。

再提醒一个容易忽略的细节:重试策略里的while条件要和 Schedule 分开理解。while决定什么错误值得重试,Schedule 决定重试的节奏。很多刚接触 Effect 的开发者把这两者混在一起,结果超时也重试、校验失败也重试,费了一堆 token 拿到同样的失败。

4. 状态管理与记忆窗口:Effect Ref 与持久化的取舍

Agent 不是无状态函数调用,它有自己的生命周期:当前轮次、累计 token、临时记忆、跨会话偏好。Fold 对状态的处理原则是一句话:循环内用内存态,跨会话用外部存储。

4.1 循环内状态用 Ref,跨会话记忆交给外部存储

Effect 自带 Ref(一种可变引用),非常适合存循环内的轻量状态,比如当前已执行的轮次数、累计 token 消耗、当前临时变量。Ref 的好处是修改操作可以组合进 Effect 链,并能在 Fiber 之间安全地协调。举个例子,我们可以在每次模型调用后更新一个计数器:

const state = yield* Ref.make({ turns: 0, tokens: 0 }); yield* Ref.update(state, (s) => ({ turns: s.turns + 1, tokens: s.tokens + response.usage.totalTokens }));

但不要试图把跨会话记忆也放进 Ref。原因很简单:Ref 是进程内内存态,进程重启、多实例部署、灰度切换都会丢数据。Agent 一旦要跨会话续聊,记忆就必须落到 Redis、数据库或专门的向量存储里。Fold 的做法是:在收敛阶段把整个会话的消息列表序列化成一份"会话存档",用户可以显式地恢复这个存档,开一个全新的 Agent 循环。这个设计也方便我们做观测——每一轮循环的状态变更都有日志,问题排查时可以直接回放。

4.2 上下文窗口管理:预算思维

每个模型都有上下文上限,而 Agent 循环天然会吞消息:一轮工具调用就可能产生好几条消息,三个工具跑一轮,上下文飞速增长。Fold 里把窗口管理写成显式的预算过程:先把系统提示、工具描述、当前用户指令这三块设为刚性预算;剩下的空间分配给历史消息。

这里用一个近乎伪代码的方式展示预算分配的过程:

  • 刚性预算:系统提示(固定长度)+ 工具描述列表(所有工具的名称和参数摘要)+ 最近的用户消息;
  • 弹性预算:历史对话。每次循环开始前计算总长度,如果超了,优先丢弃最旧的 assistant 消息,再丢 older 的 tool 消息,保持最近几轮完整;
  • 兜底策略:如果连最近一轮都放不下,那放弃部分工具描述,只保留最可能被调用的工具摘要。

这个预算策略不完美,但很实用。核心思想是"先保工具,再保历史,最后保细节"。因为工具描述如果缺失,模型根本不知道能调用什么;而历史被截断的最坏情况是模型失去一些上下文,但至少不会崩。

4.3 记忆格式对工具调用的影响

记忆格式也是我们在实战中反复调整的点。早期的 Fold 直接把工具返回的 JSON 原封不动存进历史,然后发现只要多轮工具调用,上下文马上爆炸。后来我们改成"压缩摘要":工具结果只保留结论性的字段,丢弃中间过程的冗余数据。比如数据库查询返回 100 行记录,摘要只保留"查询到 100 条记录,前 5 条为...,符合条件字段为..."

另外还有一个细节:工具调用的中间结果如果不想让模型"看到",可以塞进系统层而不是用户层。这样模型聚焦在最终答案上,不会被中间步骤干扰。但这个选择有取舍——如果 Agent 需要反思式地修正自己的工具调用,那又需要把中间结果开放给模型。Fold 目前的默认是开放,但提供了开关让上层可以关闭,这个取舍我们会在实际场景里按需调。

5. 实测中的典型坑和解决思路

最后这部分是真实的踩坑记录。Effect 确实解决问题,但它也引入了新的摩擦。这里写三个我们被绊过的地方,希望能帮你少走弯路。

5.1 类型体操过载:别被 Effect 的类型牵着走

Effect 的类型系统很强大,但代价是泛型嵌套会变得很深。特别是当你写一个复杂的 Agent 循环,既有错误类型分层又有依赖类型注入时,IDE 的自动提示经常会开始卡顿,报错信息能写满一屏。我们的经验是两个原则。

第一,面向接口编程,不要让具体 Effect 类型泄漏到业务层。在 Fold 里,Agent 对外暴露的是一个简化接口,内部实现细节全部隐藏。这样即使内部改了依赖类型,外部调用方也不会被波及。

第二,善用Effect.infer和类型别名。写 Effect 脚本时先用轻量别名把核心类型固定下来,避免在每一个函数签名里重复写一大串泛型。如果发现某处类型推导异常复杂,大概率是设计上把状态放错了位置,该抽模块而不是继续堆类型。

5.2 流式与工具调用的并发冲突

流式输出和工具调用在真实场景里经常并发出现:模型先流式吐了一段分析文字,然后在流中插入一个工具调用标记。Fold 最初对这种情况的处理是"先等流结束,再解析工具调用",但实测中流可能长达几十秒,工具请求在那里干等,整个 Agent 的响应速度被拖慢。

后来改成了边流边解析:流经过一个解析器,一旦识别出工具调用的起始标记,就立即分离出一个并发 Fiber 去准备工具执行;等内容流结束后,如果工具已就绪就直接执行。这个改动让工具调用的等待时间大幅缩短,但也引入了一个新问题:如果模型最后在流中放弃了工具调用(这种情况真的会发生),那已经准备好的工具上下文就浪费了。我们的方案是在 Fiber 里加一个短超时,超时未确认就取消,不造成太严重的浪费。

5.3 超时、取消与重试三者的边界

最后一个坑是关于三个概念的边界:超时、取消、重试。看似简单,但在 Effect 里它们各自的语义不同,组合起来很容易出问题。

举一个实际例子:一个模型调用 Effect 设置了 30 秒超时,超时触发时 Effect 会被中断,但底层 HTTP 请求不一定已经取消。如果 HTTP 客户端没有跟着中断,连接会一直占着资源。我们的修复方式是用Effect.ensuring包住整个调用,在 finally 里强制关闭客户端连接。

另一个边界问题是重试改写了超时语义。假设你对一个模型调用设置 20 秒超时,同时挂了一个 5 次重试,那么总耗时可能远远超过 20 秒。很多初学者以为"设了超时就是 20 秒内返回",但实际是"每一次尝试 20 秒,最多 5 次"。如果你真的要"整个任务最多 30 秒",那这个超时应该包在重试的外层,而不是内层。Fold 里这两种超时都有,分别叫尝试超时和总超时,语义非常明确,配置的时候不会混。

提示:在写 Agent 框架时,建议把"一次尝试的超时"和"整个任务的总超时"分开设计,并且写进文档。否则半年后你自己回头看配置,也分辨不出某个 timeout 到底管的是哪一层。

还有一个我们特别在意的设计原则:Agent 框架的所有副作用入口都收敛到 Effect 管辖区。工具注册、Provider 调用、会话持久化,全部通过 Effect 暴露给上层。这样一来,测试变的非常舒服——只需要替换掉依赖,就能跑一套完整的 Agent 流程,不需要启动任何外部服务。Fold 的自动化测试就是基于这个能力写的,每次改动代码后,模拟客户端的集成测试都能覆盖到核心循环的多数分支。

如果你正在设计自己的 Agent 框架,或者打算把一个用 Promise 写的旧 Agent 迁移到 Effect,我的建议是:先别追求一步到位。先把 Provider 层用 Schema 建模,跑通一个最简单的 Agent 循环;然后把重试和超时接上;最后再把工具链和记忆窗口加进去。每加一层,都跑一遍测试,观察一遍日志。这套路径我们走了三个月,踩坑无数,但最终的框架确实比最初的 Promise 版本结实得多。

最后分享一个我们内部的小规范:每一个 Agent 工具的执行时间都要打进日志里。不是因为要优化性能,而是因为 Agent 的行为太难预测,没有执行时间数据,你根本不知道哪一步在拖慢整体流程。这个习惯让我们的线上排查效率提高了不少,建议你也试一试。

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

OpenClaw多模型接入实战:DeepSeek与Kimi并行配置与故障切换指南

1. 先说结论&#xff1a;能连&#xff0c;但真正值钱的是连法开门见山回答标题里的问题&#xff1a;OpenClaw 能同时连接多个 AI 模型&#xff0c;而且不止 DeepSeek 和 Kimi&#xff0c;理论上只要你有 API 接入能力的模型&#xff0c;把它接进同一个 OpenClaw 实例里跑都行。…

作者头像 李华
网站建设 2026/10/22 4:57:26

智能点餐系统开发实战:Spring Boot + Vue 构建完整Web点餐平台

简介&#xff1a;这是一份面向计算机专业毕设与课程作业的智能点餐系统完整源码包&#xff0c;定位为学习型项目参考&#xff0c;涵盖了用户点餐、菜单管理、订单处理、支付集成、库存管理、数据分析及后台管理等典型模块&#xff0c;适合需要快速理解真实业务系统结构、提升前…

作者头像 李华
网站建设 2026/10/22 4:57:11

绿豆APP源码7.0:苹果CMS原生JAVA影视APP动态域名插件实战指南

简介&#xff1a;修正版绿豆APP源码7.0是一套基于苹果CMS原生JAVA的影视点播源码&#xff0c;集成木白动态域名插件&#xff0c;适合希望快速搭建自有影视APP的开发者与运营者。功能与萝卜APP基本一致&#xff0c;覆盖投屏、画中画、倍速、弹幕、下载、试看、三级分销、积分任务…

作者头像 李华
网站建设 2026/10/22 4:57:08

智能驾驶十年演进:从规则驱动到端到端大模型的行业反思

这行干久了&#xff0c;回头看总有些感慨。2016年那会儿入行&#xff0c;天天跟高精地图、毫米波雷达和一堆C代码打交道&#xff0c;那时跟朋友解释我是干什么的&#xff0c;得从“就是让车自己开”说起。现在倒是不用解释了&#xff0c;大街上的标语、手机里的推送&#xff0c…

作者头像 李华
网站建设 2026/10/22 4:57:05

智能驾驶十年演进:感知、决策与数据闭环的技术跃迁

1. 十年演进的核心逻辑&#xff1a;三个维度的同步进化如果有人问我&#xff0c;智能驾驶这十年的主线是什么&#xff0c;我会说是"从规则到学习、从单点到系统、从演示到工程"这三条线交织着往前走。十年前我们在讲"辅助驾驶能不能在高速上稳定跟车"&…

作者头像 李华
网站建设 2026/10/22 4:56:45

PyKDL与CoppeliaSim联合仿真:机械臂爬行运动实现与避坑指南

简介&#xff1a;这份资源基于PyKDL与CoppeliaSim联合仿真实现机械臂爬行运动&#xff0c;适合机器人方向的研究者、工程师及仿真学习者&#xff0c;用于解决动力学计算、轨迹规划、模型切换与状态恢复等实际开发问题。压缩包共2000个文件&#xff0c;约14.17MB&#xff0c;其中…

作者头像 李华