news 2026/10/20 23:30:38

Canvas 2D赛车开奖动画源码:时间驱动与播报机制解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Canvas 2D赛车开奖动画源码:时间驱动与播报机制解析

简介:赛车开奖动画源码是一套基于HTML、CSS与JavaScript的交互式赛车开奖动画程序,面向需要快速搭建开奖展示页面的前端开发者或活动运营者,解决开奖过程枯燥、结果可被质疑的问题。代码采用模块化目录结构,支持自定义赛车样式、赛道设计和动画速度,并预留第三方随机接口,确保开奖结果公平不可预测。资源包共40个文件,以30张PNG图片和4张JPG图片为主,提供赛车与赛道等视觉素材;2个JS文件负责动画逻辑与接口通信,2个CSS文件控制整体样式,另含1个HTML主页面和1个GIF动图,整体仅789KB,轻量易部署。目前已有745人学习查看,这类完整小巧的开奖动画源码很适合作为课程设计、活动页面的参考原型。压缩包内附完整项目结构,包含主页面与全部配套资源,开发者可在此基础上修改动画细节或接入自己的后端接口。

1. 赛车开奖动画源码:先看它能解决哪一类需求

上个月某公司的运营活动临时加了一个需求:比赛结果出来后,页面上要立刻播一段几辆车从起点冲刺到终点的动画,播完才亮名次,整体五秒内结束。运营文案里常把这类动效叫“赛车开奖动画”,技术上说,它是一个按预定结果回放的竞速播报动画。这套源码场景收敛得很明确:六辆以内赛车、一条横置赛道、前端不参与胜负计算、结果完全由业务数据驱动。它适合前端开发拿来做活动页的赛事结果展示,也适合想快速抄一套 Canvas 动效骨架、再替换成自己美术素材的开发者。接下来的内容会把它拆到能直接改配置、接接口、处理线上问题的程度。

2. 渲染选型与源码结构:为什么这次不选 WebGL

做动效先选渲染方案,这个选择一旦做错,后面所有代码都要推倒重来。这个包用的是 Canvas 2D,画面上所有车、赛道、终点线都绘制在一个<canvas>上。下面把三个候选方案的实际边界说清楚。

2.1 渲染方案三选一:CSS 动画、Canvas 2D、WebGL 的取舍

CSS 动画做“平移”确实简单,一辆车从起点到终点只需要一个transform: translateX()就能跑起来。但是这套源码的需求不止是平移:车在接近终点时要制造小幅追近的视觉效果,名次要按外部给的固定数组错峰到达,车身上要挂号码牌和名次角标。用 CSS 写这些跨元素耦合的状态会非常绕,尤其是“按 rank 推到达时间”这件事,CSS 的 timeline 很难精确控制多个对象在同一个时间轴上的先后顺序。

WebGL 在这个场景属于过度设计。六辆 2D 小车、一条横向赛道,为了它引入 shader 编译、纹理上传、顶点缓冲管理,调试成本直接翻几倍。Canvas 2D 的 API 简单直观,单帧绘制次数可控时,在主流安卓机和 iOS 设备上都能稳定跑满 60fps,这是它能被选中的根本原因。

对比维度CSS animationCanvas 2DWebGL
多车状态同步依赖 timeline,跨车耦合困难一个 update() 统一计算需要在着色器外维护状态
曲线路径与换道可做但实现绕fillRect / arc / drawImage 直接支持需要自己组装几何
贴图与换肤受样式覆盖规则影响drawImage 换图即可纹理上传有额外开销
调试成本排查样式层叠问题console.log 直接看坐标和状态要看 shader 编译日志
学习成本最低低高

结论很直接:2D 动效、对象数量在两位数以内、结果由外部数据驱动,Canvas 2D 是性价比最高的方案。这个源码没有为了炫技引入任何重量级的渲染库,核心文件加起来不到两个脚本,这个体量正好匹配它的使用场景。

2.2 源码目录与文件职责:先读哪三个文件

拿到这个包,不要急着打开 index.html 跑一遍就完事。先看结构,看清楚每个文件的职责边界,后面改起来才不会东一榔头西一棒。这套源码的文件划分很克制,核心就四份:

  • index.html:页面上只有一个#race-stage的 canvas 容器和一个结果面板,脚本放在底部,入口是 main.js。
  • css/style.css:负责页面布局、按钮、结果列表的显隐控制,不参与 canvas 内部渲染。
  • js/raceEngine.js:引擎本体。创建 canvas 上下文、维护赛车状态、跑 requestAnimationFrame 主循环、派发播报事件。这是整个包里最值得反复读的文件。
  • js/raceData.js:默认配置和演示数据。后面改主题、改配色、改赛道长宽都先看这里,不用动引擎。
  • js/main.js:入口,把按钮点击、WebSocket 消息、引擎实例串起来。

阅读顺序建议是raceData.js → raceEngine.js → main.js。先看配置,知道有哪些可调参数;再看引擎的start()与update()两个方法,理解动画是怎么被驱动起来的;最后回到入口,看业务数据是怎么喂进去的。

2.3 画布尺寸与自适应:写死宽高是第一个坑

很多人在接这种动效时第一件事就是把 canvas 的宽高写死成 1920×600。这个源码的做法是:用container.getBoundingClientRect()读取容器实际宽高,再拿window.devicePixelRatio做高清屏适配,最后赋值给 canvas 的width和height属性。注意canvas.style.width是 CSS 像素尺寸,canvas.width是位图像素尺寸,这两者不能混用。

// main.js 画布初始化片段 const stage = document.getElementById('race-stage'); const rect = stage.getBoundingClientRect(); const dpr = Math.max(window.devicePixelRatio || 1, 1); canvas.width = Math.round(rect.width * dpr); canvas.height = Math.round(rect.height * dpr); canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

这段代码里setTransform(dpr, 0, 0, dpr, 0, 0)是关键。它把后续所有绘制坐标统一换算成 CSS 像素坐标,写坐标时不用再手动乘 dpr。初始化之后这段逻辑不要重复执行,否则 canvas 位图会被重新分配,已经画好的离屏缓存全部失效。这个细节在后文的避坑章节还会遇到。

3. 核心动画拆解:从赛道初始化到名次撞线

这一章进入引擎内部。赛车开奖动画的难点不在“画一辆车”,而在“让多辆车按预定名次错峰到达终点,同时视觉上还保留悬念”。源码里最核心的设计都在这里。

3.1 数据模型与赛道参数:车、道、距都从配置来

赛车、赛道、时间这三个维度的参数全部集中在raceData.js的defaultConfig对象里。这个包的默认参数针对六车道、五秒内播完的场景标定,改业务时优先动这里。

// raceData.js 核心配置 const defaultConfig = { laneCount: 6, // 赛道数量,超过 6 视觉上会拥挤 laneHeight: 56, // 单条赛道的像素高 startX: 70, // 起点线 X 坐标 finishX: 920, // 终点线 X 坐标 baseDuration: 4200, // 第一名从起点到终点的时间,单位 ms rankGapMs: 180, // 相邻名次到达终点的时间差,越小悬念越足 trackSpriteUrl: '', // 赛道背景图片,空字符串时用纯色绘制 racerSpriteSize: 48 // 赛车贴图尺寸,会做离屏裁剪 };

baseDuration和rankGapMs是整套动画的节奏灵魂。baseDuration决定第一名多久到达,rankGapMs决定第二名比第一名晚多少毫秒撞线。它们之间是线性叠加关系:假设第一名 4200ms 到达,第二名就是 4380ms,第三名 4560ms,依此类推。这个设计比“每辆车固定速度”高级的地方在于,它能保证后车永远追不上前车,但视觉上又是同时加速冲线。

赛车建模同样简洁:

// raceEngine.js 赛车建模 class Racer { constructor(info, laneIndex, config) { this.name = info.name; this.color = info.color; this.rank = info.rank; // 预定名次,1 表示最先撞线 this.laneIndex = laneIndex; // 从 0 开始的车道序号,只决定位置 this.progress = 0; // 0~1,1 表示已撞线 this.x = config.startX; this.y = config.trackTop + laneIndex * config.laneHeight + config.laneHeight / 2; this.sprite = buildRacerSprite(config.racerSpriteSize, info.color); } }

注意laneIndex和rank是彻底解耦的。车道序号只决定这辆车摆在哪条跑道,rank决定它什么时候到达终点。这样业务方传排名数组时,不需要关心车停在哪条道,引擎内部根据 rank 推导时间,根据 laneIndex 摆放位置。

3.2 主循环与位移方程:时间差才是进度,帧数不是

主循环是动画发动机。很多初学写的动效会在requestAnimationFrame里靠“每帧移动 3 像素”这种方式推位移,一旦碰到帧率波动,车就会忽快忽慢。这个源码用的是时间差驱动:每次 rAF 回调都会收到一个now时间戳,用它减去动画开始时间得到 elapsed,再除以目标时长得到进度,最后映射到 X 坐标。

// raceEngine.js 主循环骨架 start(payload) { if (this.state === 'running') { this.reset(); // 防重入:上一场还没播完就切新数据 } this.buildRacers(payload.racers); this.state = 'running'; this.startTime = performance.now(); const loop = (now) => { if (this.state !== 'running') return; this.update(now); this.render(); if (this.racers.every(r => r.progress >= 1)) { this.state = 'settled'; this.emit('settle', this.buildRanking()); return; } this.rafId = requestAnimationFrame(loop); }; this.rafId = requestAnimationFrame(loop); }

loop是箭头函数,这样它绑定的是创建时的this,回调里访问this.state不会丢上下文。this.rafId被保存下来,是因为后面reset()里需要cancelAnimationFrame挂起这条链,避免上一场的循环还在跑、下一场的循环又开始跑。

位移推进全部在update()里完成:

// raceEngine.js 位移推进 update(now) { const elapsed = now - this.startTime; for (const r of this.racers) { if (r.progress >= 1) continue; const rawT = elapsed / this.targetTimeOf(r); // 0~1 线性进度 const t = Math.min(Math.max(rawT, 0), 1); r.progress = easeInOutCubic(t); r.x = this.config.startX + (this.config.finishX - this.config.startX) * r.progress; r.y = this.config.trackTop + r.laneIndex * this.config.laneHeight + this.config.laneHeight / 2; } } targetTimeOf(r) { return this.config.baseDuration + (r.rank - 1) * this.config.rankGapMs; }

位移曲线用的是 easeInOutCubic:前半段加速慢,中段咬合紧,最后一段减速。如果改成线性函数,第一名会从头领先到尾,观众一眼看穿结局,播报动效的“仪式感”就没了。

这里解释一个很容易踩的误区:rawT除以的是targetTimeOf(r),这个值对每辆车是不同的。后车的时间线比前车短,但因为曲线是缓进的,后车在视觉上前半段不会立刻被甩开,而是到冲刺阶段才体现差距。这种“前面咬住,终点才分离”的效果,正好符合赛事播报的节奏。

3.3 结果怎么演出来:rank 决定到达时间,速度只是包装

这一部分是整个源码里最反直觉的设计,也是它区别于普通“赛车游戏”的地方。普通赛车动画是每辆车按自己的速度跑,谁先到终点谁赢;开奖播报类动画是结果已经定好,动画只是把结果演出来。业务方给一个 rank 数组,引擎为每个 rank 分配一个到达时间,然后用位移曲线把这段时间填满。

视觉包装上有几个细节:

  • 终点线前 10% 的距离,领先车会被曲线强制减速,后车在同一区间内加速追赶,但永远不会超过前车,因为后车的 targetTime 本来就比前车长。
  • rankGapMs越小,所有车撞线时间越密集,悬念感越强。默认 180ms 会让人觉得是“同时冲线但拍照判定分出先后”,这个体感是大多数运营想要的。
  • 撞线发生后,progress被钳制在 1,X 坐标停在finishX,后续帧不再推进,但车仍然被绘制在画面上,直到reset()被调用。

渲染层同样克制:

// raceEngine.js 渲染层 render() { const ctx = this.ctx; ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); ctx.drawImage(this.trackSprite, 0, 0); ctx.fillStyle = '#f8f9fa'; ctx.fillRect(this.config.finishX - 4, 0, 4, this.canvasHeight); for (const racer of this.racers) { ctx.drawImage(racer.sprite, racer.x - 24, racer.y - 28); } }

赛道背景trackSprite和每辆车的sprite都是离屏预渲染出来的:初始化时先在内存 canvas 上画好,运行时每帧只做drawImage拷贝。整帧的绘制调用被压缩到十次以内,这是它能跑满 60fps 的根本原因,而不是靠什么黑魔法。

4. 结果播报接入:把固定排名转成动画事件

源码和业务之间的耦合点只有两个:一个数据入口(启动动画),一个数据出口(结算回调)。看明白这两个口,接任何后端结果推送都不需要改引擎。

4.1 播报数据结构与状态机:输入输出都是纯数据

业务方推到前端的播报数据,结构非常轻量:

// 业务方推送的播报数据 const racePayload = { raceId: 'R20240112-03', racers: [ { name: '疾风', color: '#e03131', rank: 1 }, { name: '猎鹰', color: '#1971c2', rank: 2 }, { name: '闪电', color: '#f08c00', rank: 3 }, { name: '银翼', color: '#868e96', rank: 4 } ] };

引擎只认racers数组里的name / color / rank三个字段,raceId只是透传给回调,不做内部逻辑。这个设计保证了业务层可以自由扩展字段,比如加 headImg、加赔率信息,引擎不会因为多一个字段就崩。

状态机固定在三种状态之间切换:

状态触发条件可执行操作
idle构建完成或 reset 后可调用 start(payload)
runningstart(payload) 被调用更新进度、渲染、可取消或重置
settled所有 racer.progress >= 1读取结果,重播必须先 reset

这套状态机的价值在于,外部调用方任何时候只需要检查state字段就能决定下一步动作,不需要关心引擎内部的 rAF 链状态。

4.2 初始化、回调与 WebSocket 对接:动效要挂到业务链路上

初始化发生在入口脚本main.js,引擎实例化时传入配置和结算回调:

// main.js 初始化引擎 const raceEngine = new RaceEngine(document.getElementById('race-stage'), { baseDuration: 4200, rankGapMs: 180, onSettle: (result) => { // result 已按 rank 排序完成,可直接渲染到结果面板 renderResultList(result); } }); // 按钮触发 document.getElementById('play-btn').addEventListener('click', () => { if (raceEngine.state === 'running') return; // 防连点 raceEngine.start(loadDemoPayload()); });

实际业务中,结果几乎不会来自本地按钮,而是来自服务端推送。常见做法是走 WebSocket 长连接,推送到达的瞬间启动动画:

// 假设业务用 WebSocket 推送比赛结果 const socket = new WebSocket('wss://your-host/race-channel'); socket.addEventListener('message', (event) => { const message = JSON.parse(event.data); if (message.type === 'race.result') { raceEngine.reset(); // 关键:先复位上一场状态 raceEngine.start(message.payload); } });

reset()是很容易漏掉的一步。如果不先复位,上一场的赛车会残留在画布上,新车再挤上去,画面就叠成一团。源码里start()内部其实也带了一次 reset 保护,这段代码保留显式 reset 是为了可读性和边界防御,线上环境宁可多一次空操作,也不能赌调用时机刚刚好。

4.3 可复用配置参数:一个引擎跑多个主题

整套源码的扩展性都集中在一个配置对象上。除了前面看到的baseDuration和rankGapMs,还有几个适合业务接入时调整的项:

配置项默认值说明
laneCount6赛道数,同时决定画布高度基准
laneHeight56单赛道高,数值越小赛道越紧凑
baseDuration4200第一名到达终点的毫秒数,越长越有仪式感
rankGapMs180相邻名次撞线时间差,设 120~180 更有悬念,300 以上名次分明
showRankTagtrue撞线后是否展示名次角标
autoResetfalsesettle 后多少毫秒自动复位,false 表示手动复位

我给这个参数提一个使用建议:rankGapMs是这个包里最值得调的旋钮,但它的上限不要超过 1000。超过 1000 毫秒,观众在最慢的车还没跑过一半时就可能离开页面。同理,baseDuration低于 2500ms 时胜负感太急促,高于 6000ms 时用户会觉得拖沓,默认的 4200ms 是多数活动页能接受的临界点。

5. 避坑排查:五个高频问题与现场追因

动画本身不难,难的是各种边界情况下动画表现不一致。这一章写的是从实际项目里带出来的五条踩坑记录,源码里不会主动注释这些,全靠运行期暴露。每条按“现象 → 原因 → 解决”拆开,可以直接对照排查。

5.1 切后台回来动画瞬移

现象:页面切到后台再切回来,播报动画直接跳到结尾,或者某辆车瞬间出现在终点线。

原因:这个坑来自用固定步长累加位移的写法——每帧x += 5,浏览器切后台时 rAF 暂停,步长累加停止,但现实时间在走。回到前台时,浏览器连续补帧,引擎用剩余的数据一次性跳到终点,表现就是瞬移。

解决:位移推进一律改用 rAF 回调里的now时间戳,计算elapsed = now - startTime,再除以目标时间得到进度。这样切后台再回来,elapsed会一次性跳到真实值,但update()里会对t做了clamp(0, 1)钳制,结果只是直接切到结算状态,不会出现中间状态的乱跳。

5.2 低端机掉帧与机身发热

现象:在某千元机上跑,帧率只有 20fps 出头,播完一场动画手机背部明显发热。

原因:每帧都对赛车做了十几条路径绘制——轮胎、车壳、尾翼、阴影、号码牌,加上赛道划线全部用fillRect逐条打,单帧绘制调用超过 150 次。旧设备的 2D canvas 光栅化线程被持续拉满,发热随之而来。

解决:把赛车的静态造型提前画到离屏 canvas 上,运动时只drawImage整张图。赛道背景(草地、柏油、起点网格线)也离屏画一次。优化后单帧绘制次数降到两位数以内,这个场景在低端机上也能守住 40fps 以上。

5.3 高分屏画面发虚

现象:在 Retina 屏幕上,车身轮廓和号码牌边缘都毛毛糙糙,像位图被硬拉大了一个尺寸。

原因:canvas 元素的 CSS 宽度和内部位图像素宽度不一致。高分屏默认把 canvas 位图按逻辑分辨率放大绘制,没有做devicePixelRatio适配,等于一张 1x 的图被强行拉伸到 3x 显示。

解决:初始化时读取window.devicePixelRatio,把canvas.width设为cssWidth * dpr,再调用ctx.setTransform(dpr, 0, 0, dpr, 0, 0)。后面所有坐标继续按 CSS 像素写,清晰度立刻上一个档次。

5.4 终点前提前剧透名次

现象:动画播到一半,领先车已经拉开两个身位,结果毫无悬念,运营反馈“节奏不对,像假赛”。

原因:rankGapMs设得太大,例如 500ms 以上。领先车在 60% 进度时已经领先了视觉上一个多车位,悬念在动画中途就耗尽了。

解决:把rankGapMs缩到 120~180ms 之间,并确认位移曲线用的是终点前减速的easeInOutCubic。目视感受是前车中段加速甩开,最后一段后车贴上来但超不过,撞线前后才分出名次。

5.5 快速连点导致画面叠车

现象:一场播完立刻点“再播一次”,画面上出现两批赛车混跑,或者上一场的车定在终点不动。

原因:start()没有在动画运行中做保护,或者reset()里没有取消上一场挂起的 rAF,导致同一个 canvas 上同时跑两条渲染链。

解决:在start()入口判断state === 'running'时先执行reset();reset()内部必须调用cancelAnimationFrame(this.rafId),再把每辆车的progress清零、X 坐标复位到起点。外部再在按钮上做一层防抖,双保险才能把这种手滑操作挡在引擎外。

6. 验证与扩展:把播报动画做成基础组件

6.1 用 Performance API 做性能预算验证

接入这套源码后,不要只看桌面浏览器上跑得顺就交付。在loop()里临时加一段耗时埋点,用数据说话:

// 临时验证帧耗时,正式发布前删掉 const t0 = performance.now(); this.update(now); this.render(); const cost = performance.now() - t0; if (cost > 10) { console.warn('单帧耗时超标:', cost.toFixed(2) + 'ms'); }

判定线参考:连续 100 帧平均耗时 ≤ 8ms,且单帧超过 10ms 的次数不超过 5%,就具备中端机 60fps 的余量。如果超标,优先检查三个位置:drawImage次数是否暴增、是否每帧用fillText重绘了动态文本、有没有在绘制路径上开启全局 shadow。这三个位置几乎覆盖了这种动效的绝大多数性能问题。

6.2 三个扩展技巧:慢速回放、主题切换、音效联动

慢速回放不用改状态机。播报数据里增加一个durationFactor字段,把baseDuration乘上这个系数即可,比如 1.8 就是 1.8 倍慢放。它与rankGapMs是相乘关系而不是相减关系,慢放时名次间距也被等比例拉长,逻辑上不会乱。

主题切换的核心是把配色、贴图 URL、终点样式全部收进一个theme对象放在raceData.js。引擎只认theme字段,不认业务名。换赛事主题时只替换theme,绝不复制整个引擎。

音效联动放在onSettle回调里触发,撞线瞬间同时播放短促的收尾音。很多项目动效和音效分开走两个 timer,结果音效比画面慢半拍,正确做法是让音频在onSettle同一帧触发,这样才能做到声画同步。

从接手这个包之后我定了一条工作流:凡是接类似的播报动效,先确认三件事——结果是不是后端给固定数组、页面会不会被切后台、最低端测试机是什么级别。然后每次改完配置,都强制走一遍三连验证——切后台回前台、低端机试跑一次、快速双击按钮再触发一次。这套源码我现在一直当内部基础组件用,换业务只改数据和主题,引擎本身很少再动。希望帮到你。

本文还有配套的精品资源,点击获取

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

Source Insight C/C++代码阅读实战指南:嵌入式与驱动开发必备

1. 这不是普通代码阅读器&#xff0c;而是一把能“剖开”百万行C/C项目的手术刀Source Insight 这个名字听起来平平无奇&#xff0c;但在我过去十年带过的十几个嵌入式、驱动和大型中间件项目里&#xff0c;它几乎从不缺席。我见过太多团队在项目初期用VS Code配一堆插件、折腾…

作者头像 李华
网站建设 2026/10/20 23:29:12

ASP.NET Web Forms轻量排班系统:XML存储+IIS零数据库部署

简介&#xff1a;这是一套基于ASP.NET Web Forms开发的值班管理信息系统&#xff0c;面向初学者与中小型企事业单位信息化建设者&#xff0c;解决排班计划制定、人员在岗状态跟踪、值班记录归档等日常行政管理痛点。资源包共77个文件&#xff0c;含31个核心aspx页面&#xff08…

作者头像 李华
网站建设 2026/10/20 23:28:22

链表增删查改全解析:从指针操作到工程实战

很多人刚接触数据结构时&#xff0c;第一个真正需要“手动操作内存”的难题就是链表的增删查改。数组好歹有连续下标可以顺手拿&#xff0c;链表却要用指针一个节点一个节点地跳&#xff0c;很多初学者在这里第一次体会到什么叫“程序跑崩了但不知道崩在哪”。但链表绝对不是面…

作者头像 李华
网站建设 2026/10/20 23:27:58

ASP.NET值班管理系统:排班规则建模与EF Core领域设计

简介&#xff1a;这是一套基于ASP.NET Web Forms开发的值班管理信息系统&#xff0c;面向初学者与中小型单位信息化建设者&#xff0c;解决排班登记、人员调度、值班记录查询等日常行政管理痛点。系统采用B/S架构&#xff0c;适配Windows Server 2003VS2010开发环境&#xff0c…

作者头像 李华
网站建设 2026/10/20 23:25:24

域环境下的信息收集:从活动目录到组策略的实操指南

1. 域环境里的"信息收集"到底在收集什么&#xff1a;先讲清楚概念很多刚接触内网安全的朋友第一次听到"域环境下的信息收集"这个说法&#xff0c;第一反应是"拿工具扫一扫"。实际上&#xff0c;在活动目录的环境里做信息摸排&#xff0c;和我们平…

作者头像 李华
网站建设 2026/10/20 23:25:14

LangGraph实战:从链式到图编排,构建可控的大模型工作流

1. 从“链”到“图”&#xff1a;为什么工作流编排是大模型应用的分水岭1.1 线性链的局限&#xff1a;当业务逻辑开始分叉在LangGraph出现之前&#xff0c;多数基于大模型的应用都是“链式”结构&#xff1a;先把用户问题拼成提示词&#xff0c;丢给模型&#xff0c;拿回结果&a…

作者头像 李华