我在设计工具里实现了一个 Agent Team:多智能体协作生成 UI 的实战经验

前言

OpenPencil(一个开源矢量设计工具)的 AI 设计生成功能中,我实现了一套多智能体编排系统(Agent Team)。用户输入一句话描述,系统会自动将其拆解为多个子任务,分配给不同的 Sub-Agent 并行生成,最终在画布上实时呈现完整的 UI 设计。

本文将从架构设计、核心实现、并发控制、流式渲染等角度,详细拆解这套系统的实现过程。

一、为什么需要 Agent Team?

单 Agent 的瓶颈

最初的设计生成是"一锅端":把整个 UI 描述扔给一个 LLM,让它一次性输出所有节点。问题很快暴露:

  1. 上下文爆炸:一个完整的 Landing Page 可能包含 100+ 节点,LLM 在后半段容易"遗忘"前面的设计约束
  2. 速度太慢:复杂页面需要 30-60 秒才能完成,用户盯着空白画布等待
  3. 错误传播:一个区域出错(比如 Hero 排版溢出),后续所有区域都会受到影响
  4. 无法并行:单次 API 调用是串行的,无法利用多路并发加速

Agent Team 的思路

借鉴软件工程中的分治策略:

graph TD O["Orchestrator
(快速规划 Agent)"] O -->|OrchestratorPlan| A1["Agent #1 "Kiki"
导航栏"] O -->|OrchestratorPlan| A2["Agent #2 "Mochi"
Hero 区"] O -->|OrchestratorPlan| A3["Agent #3 "Pixel"
特性区"] A1 -->|JSONL| Canvas["实时画布渲染 + 动画"] A2 -->|JSONL| Canvas A3 -->|JSONL| Canvas

每个 Agent 只负责一个空间区域,互不干扰,独立生成,最终合并到同一个画布。

二、五阶段流水线架构

整个 Agent Team 的执行分为 5 个阶段:

typescript 复制代码
// orchestrator.ts - 主入口
export async function executeOrchestration(
  request: AIDesignRequest,
  callbacks?: {
    onApplyPartial?: (count: number) => void  // 节点插入回调
    onTextUpdate?: (text: string) => void     // 进度更新回调
    animated?: boolean                         // 是否启用动画
  },
  abortSignal?: AbortSignal,
): Promise<{ nodes: PenNode[]; rawResponse: string }>

Phase 1:Planning --- 编排 Agent 做任务分解

第一阶段用一个轻量级的 Orchestrator Agent 将用户的设计描述拆解为多个空间子任务。这个 Agent 不做任何实际的设计生成,只做"分活"。

typescript 复制代码
const plan = await callOrchestrator(
  preparedPrompt.orchestratorPrompt,
  preparedPrompt.originalLength,
  request.model,
  request.provider,
  (thinking) => renderPlanningStatus(thinking), // 实时展示思考过程
  abortSignal,
)

Orchestrator 的 system prompt 经过精心设计,只做一件事------结构化分解:

typescript 复制代码
export const ORCHESTRATOR_PROMPT = `Split a UI request into cohesive subtasks.
Each subtask = a meaningful UI section or component group.
Output ONLY JSON, start with {.

DESIGN TYPE DETECTION:
1. Multi-section page → width=1200, height=0, 6-10 subtasks
2. Single-task screen → width=375, height=812, 1-5 subtasks
3. Data-rich workspace → width=1200, height=0, 2-5 subtasks
...`

输出是一个严格的 JSON 结构:

json 复制代码
{
  "rootFrame": {
    "id": "page",
    "name": "Landing Page",
    "width": 1200,
    "height": 0,
    "layout": "vertical"
  },
  "styleGuide": {
    "palette": {
      "background": "#F8FAFC",
      "accent": "#6366F1",
      "text": "#0F172A"
    },
    "fonts": { "heading": "Space Grotesk", "body": "Inter" },
    "aesthetic": "clean modern with purple accents"
  },
  "subtasks": [
    {
      "id": "nav",
      "label": "Navigation Bar",
      "elements": "logo, nav links, sign-in button, CTA button",
      "region": { "width": 1200, "height": 72 }
    },
    {
      "id": "hero",
      "label": "Hero Section",
      "elements": "headline, subtitle, CTA, illustration",
      "region": { "width": 1200, "height": 560 }
    }
  ]
}

这里有几个关键的设计决策:

1. 元素边界(Element Boundaries)

每个子任务必须声明自己负责哪些 UI 元素,且元素不能跨子任务重叠。这是防止"两个 Agent 同时生成了提交按钮"这类冲突的关键:

less 复制代码
subtask "Login Form":  elements = "email input, password input, submit button"
subtask "Social Login": elements = "Google button, Apple button, divider"
                                    ↑ 不会重复生成 submit button

2. 统一的 Style Guide

Orchestrator 生成一份全局色板和字体方案,所有 Sub-Agent 共享这份 Style Guide。这保证了即使多个 Agent 并行工作,最终产出的颜色和风格也是一致的。

3. 空间区域(Region)

每个子任务带有目标区域尺寸(宽高),Sub-Agent 据此控制内容量,不会生成过多或过少的节点。

Phase 2:Canvas Setup --- 创建画布骨架

根据 Plan 在画布上创建根容器。对于并发模式(多屏设计),系统会为每组屏幕创建独立的根 Frame:

typescript 复制代码
if (effectiveConcurrency > 1) {
  // 并发模式:每个 screen group 一个根 Frame
  for (let g = 0; g < screenGroups.length; g++) {
    const rootNode: FrameNode = {
      id: `${plan.rootFrame.id}-${group.screen}`,
      type: 'frame',
      name: frameName,
      x: nextX,  // 横向排列
      y: 0,
      width: plan.rootFrame.width,
      height: frameHeight,
      layout: 'vertical',
      children: [],
    }
    // 第一个 frame 走 insertStreamingNode 处理空画布替换
    // 后续 frame 直接 addNode,避免 ID 重映射冲突
    if (g === 0) {
      insertStreamingNode(rootNode, null)
    } else {
      addNode(null, rootNode)
    }
    nextX += plan.rootFrame.width + 100  // 屏幕间距 100px
  }
} else {
  // 顺序模式:所有子任务共用一个根 Frame
  const rootNode: FrameNode = { ... }
  insertStreamingNode(rootNode, null)
}

Phase 3:Sub-Agent 执行 --- 核心生成阶段

这是最复杂也最精彩的部分。每个子任务会被分配给一个 Sub-Agent,以 JSONL 流式格式输出 PenNode 节点。

ID 命名空间隔离

这是并发安全的核心机制。每个 Sub-Agent 的所有节点 ID 必须带上所属子任务的前缀:

typescript 复制代码
export function ensureIdPrefix(node: PenNode, prefix: string): void {
  if (!node.id.startsWith(`${prefix}-`)) {
    node.id = `${prefix}-${node.id}`
  }
  if ('children' in node && Array.isArray(node.children)) {
    for (const child of node.children) {
      ensureIdPrefix(child, prefix)
    }
  }
}

这样即使两个 Agent 都生成了 id: "title" 的节点,实际写入画布的是 hero-titlefeatures-title,不会冲突。

流式 JSONL 解析

Sub-Agent 输出的格式是 JSONL(每行一个 JSON 节点),通过 _parent 字段表达树形关系:

jsonl 复制代码
{"_parent":null,"id":"root","type":"frame","name":"Hero","width":"fill_container","height":"fit_content"}
{"_parent":"root","id":"title","type":"text","name":"Headline","content":"Learn Smarter","fontSize":48}
{"_parent":"root","id":"cta","type":"frame","name":"CTA Button","role":"button","width":180}
{"_parent":"cta","id":"cta-text","type":"text","content":"Get Started","fontSize":16}

流式解析器在每个 token 到达时就尝试提取完整的 JSON 对象:

typescript 复制代码
// extractStreamingNodes --- 在 rawResponse 上增量解析
const { results, newOffset } = extractStreamingNodes(rawResponse, streamOffset)

for (const { node, parentId } of results) {
  ensureIdPrefix(node, subtask.idPrefix)          // 加 ID 前缀
  addAgentIndicatorRecursive(node, agentColor, agentName)  // 加视觉标记
  markNodesForAnimation([node])                    // 标记动画
  insertStreamingNode(node, prefixedParent)        // 插入画布
}

每个节点在解析出来的瞬间就被插入画布,用户能看到 UI 元素像"打字机"一样逐个出现。

并发控制:信号量 + 屏幕分组

并发执行使用了一个手动实现的**信号量(Semaphore)**来限制同时进行的 API 调用数:

typescript 复制代码
// 信号量实现
let activeSlots = 0
const waitQueue: (() => void)[] = []

async function acquireSlot() {
  if (activeSlots < concurrency) {
    activeSlots++
    return
  }
  // 如果槽位满了,等待释放
  await new Promise<void>((resolve) => waitQueue.push(resolve))
  activeSlots++
}

function releaseSlot() {
  activeSlots--
  if (waitQueue.length > 0) {
    waitQueue.shift()!()  // 唤醒等待中的 Agent
  }
}

同一屏幕内的子任务仍然串行执行 (保证 Navigation → Hero → Features 的顺序),不同屏幕之间并行执行

typescript 复制代码
// 每个 screen group 内部串行,不同 group 之间并行
const workers = screenGroups.map(async (indices) => {
  for (const idx of indices) {
    if (abortSignal?.aborted) return
    await acquireSlot()
    try {
      const result = await executeSubAgent(plan.subtasks[idx], ...)
      // 插入节点后扩展根 frame 高度
      if (result.nodes.length > 0) {
        expandRootFrameHeight(plan.subtasks[idx].parentFrameId)
      }
    } finally {
      releaseSlot()
    }
  }
})

await Promise.all(workers)

为什么不全部并行?因为垂直布局的页面,上下区域的视觉连贯性很重要。如果 Footer 比 Hero 先生成完毕,用户看到的就是颠倒的页面。

Phase 4:后处理

所有 Sub-Agent 完成后,执行后处理:

  • adjustRootFrameHeightToContent():根据实际内容调整根 Frame 高度
  • applyPostStreamingTreeHeuristics():应用基于树结构的启发式规则(按钮宽度、Frame 高度、clipContent 等)
  • zoomToFitContent():自动缩放画布以展示完整设计

Phase 5:视觉校验(可选)

利用 Vision API 对生成的设计截图进行自动校验,检测并修复视觉问题:

typescript 复制代码
const validationResult = await runPostGenerationValidation({
  onStatusUpdate: (status, message) => {
    validationEntry.status = status
    validationEntry.thinking = message
    emitProgress(plan, progress, callbacks)
  },
  model: request.model,
  provider: request.provider,
})

三、Agent 身份系统

在并发模式下,多个 Agent 同时在画布上工作。为了让用户直观地看到"谁在画什么",我设计了一套 Agent Identity 系统:

typescript 复制代码
// agent-identity.ts
const AGENT_COLORS = [
  '#FF6B6B', // coral red
  '#4ECDC4', // teal
  '#FFD93D', // golden yellow
  '#6C5CE7', // purple
  '#A8E6CF', // mint green
  '#FF8A5C', // warm orange
]

const AGENT_NAMES = [
  'Kiki', 'Mochi', 'Pixel', 'Nova', 'Zuri', 'Cleo',
  'Boba', 'Rune', 'Fern', 'Echo', 'Puck', 'Sage',
]

export function assignAgentIdentities(count: number): AgentIdentity[] {
  // Fisher-Yates 洗牌名字
  const shuffled = [...AGENT_NAMES]
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
  }
  return Array.from({ length: count }, (_, i) => ({
    color: AGENT_COLORS[i % AGENT_COLORS.length],
    name: shuffled[i % shuffled.length],
  }))
}

每个 Agent 在画布上的节点会显示带颜色的标记和名字(比如紫色的 "Pixel" 正在生成 Features 区域),Sub-Agent 完成后标记自动淡出:

typescript 复制代码
// 完成后延迟移除标记,让用户能看到完成效果
setTimeout(() => removeAgentIndicatorsByPrefix(subtask.idPrefix), 1500)

标记通过 globalThis 上的共享 Map 管理,避免了 Vite 模块分割导致的状态隔离问题:

typescript 复制代码
// agent-indicator.ts --- 使用 globalThis 保证单例
const INDICATORS_KEY = '__openpencil_agent_indicators__'

function getIndicatorMap(): Map<string, AgentIndicatorEntry> {
  const g = globalThis as Record<string, unknown>
  if (!g[INDICATORS_KEY]) {
    g[INDICATORS_KEY] = new Map<string, AgentIndicatorEntry>()
  }
  return g[INDICATORS_KEY] as Map<string, AgentIndicatorEntry>
}

四、实时进度系统

用户在聊天面板中能看到每个 Agent 的实时状态,这通过 <step> 标签协议实现:

typescript 复制代码
export function emitProgress(
  plan: OrchestratorPlan,
  progress: OrchestrationProgress,
  callbacks?: { onTextUpdate?: (text: string) => void },
): void {
  const planningStep = '<step title="Planning layout" status="done">...</step>'

  const subtaskSteps = plan.subtasks
    .map((st, i) => {
      const entry = progress.subtasks[i]
      const nodeInfo = entry.nodeCount > 0 ? ` (${entry.nodeCount} elements)` : ''
      return `<step title="${st.label}${nodeInfo}" status="${entry.status}">${entry.thinking ?? ''}</step>`
    })
    .join('\n')

  callbacks.onTextUpdate(`${planningStep}\n${subtaskSteps}`)
}

聊天面板解析这些 <step> 标签渲染为一个 Checklist UI:

css 复制代码
✅ Planning layout
🔄 Navigation Bar (8 elements)       ← Agent "Kiki" 正在生成
⏳ Hero Section                       ← 等待中
⏳ Feature Cards                      ← 等待中
⏳ Footer                             ← 等待中

五、Sub-Agent 的 Prompt 工程

Sub-Agent 的 prompt 构建是整个系统中最需要精心打磨的部分。每个 Sub-Agent 收到的上下文包含:

1. 全局上下文(知道整体结构但不越界)

typescript 复制代码
// 展示所有区域,标记当前 Agent 负责的区域
const sectionList = plan.subtasks
  .map((st) => {
    const marker = st.id === subtask.id ? ' ← YOU' : ''
    const elems = st.elements ? ` [${st.elements}]` : ''
    return `- ${st.label}${elems} (${st.region.width}x${st.region.height})${marker}`
  })
  .join('\n')

输出效果:

less 复制代码
Page sections:
- Navigation Bar [logo, nav links, CTA button] (1200x72)
- Hero Section [headline, subtitle, illustration] (1200x560) ← YOU
- Feature Cards [3 cards with icon + title] (1200x480)
- Footer [links, copyright] (1200x200)

Generate ONLY "Hero Section" (~560px of content).
YOUR ELEMENTS: headline, subtitle, illustration
Do NOT generate elements listed in other sections.

2. 统一的 Style Guide 注入

typescript 复制代码
if (plan.styleGuide) {
  prompt += `\nSTYLE GUIDE (use these consistently):
- Background: ${p.background}  Surface: ${p.surface}
- Text: ${p.text}  Secondary: ${p.secondary}
- Accent: ${p.accent}  Border: ${p.border}
- Heading font: ${sg.fonts.heading}  Body font: ${sg.fonts.body}
- Aesthetic: ${sg.aesthetic}`
}

3. 条件性指令注入

根据内容特征动态注入额外约束,而非一次性塞入所有规则:

typescript 复制代码
// 检测到密集卡片场景 → 注入精简卡片指令
if (needsNativeDenseCardInstruction(subtask.label, compactPrompt, fullPrompt)) {
  prompt += `\nNATIVE DENSE-CARD MODE:
- Each card: max 2 text blocks only (title + one short metric)
- Rewrite long copy into concise keyword phrases
- Never use truncation marks ("..." or "...")`
}

// 检测到表格场景 → 注入表格结构指令
if (needsTableStructureInstruction(subtask.label, compactPrompt, fullPrompt)) {
  prompt += `\nTABLE MODE:
- Build table as explicit grid frames, NOT a single long text line
- Header must be its own horizontal row with separate cell frames`
}

这种按需注入策略避免了 prompt 膨胀------一个简单的导航栏不需要看到 20 条表格排版规则。

六、容错与降级策略

Agent Team 系统的健壮性来自多层容错:

1. Orchestrator 失败 → 启发式降级

如果编排 Agent 无法返回有效的 Plan,系统会根据用户 prompt 启发式地生成一个默认 Plan:

typescript 复制代码
// orchestrator-prompt-optimizer.ts
export function buildFallbackPlanFromPrompt(prompt: string): OrchestratorPlan {
  // 根据关键词检测设计类型
  const isMobile = /mobile|移动|手机|login|登录|profile/i.test(prompt)
  const width = isMobile ? 375 : 1200
  const height = isMobile ? 812 : 0
  // 生成一个简单的单区域 Plan
  return {
    rootFrame: { id: 'page', width, height, layout: 'vertical' },
    subtasks: [{ id: 'main', label: 'Main Content', region: { width, height: 600 } }],
  }
}

2. 流式解析失败 → 批量回退

如果流式 JSONL 解析器没有提取到任何节点(可能是 LLM 输出格式偏差),在流结束后尝试批量解析:

typescript 复制代码
// 流式解析失败时的后备方案
if (nodes.length === 0 && rawResponse.trim().length > 0) {
  const fallbackNodes = extractJsonFromResponse(rawResponse)
  if (fallbackNodes && fallbackNodes.length > 0) {
    for (const node of fallbackNodes) {
      ensureIdPrefix(node, subtask.idPrefix)
      insertStreamingNode(node, targetParent)
    }
  }
}

3. 部分失败不影响整体

某个 Sub-Agent 超时或报错时,只有该区域标记为 error,其他已完成的区域正常保留:

typescript 复制代码
const collected = results.filter((r): r is SubAgentResult => r !== null)
const totalNodes = collected.reduce((sum, r) => sum + r.nodes.length, 0)

// 只有当所有 Agent 都返回 0 个节点时才抛错
if (totalNodes === 0 && collected.length > 0) {
  throw new Error('All sub-agents failed')
}

4. 用户随时中断

通过 AbortSignal 实现优雅的中断------已生成的节点保留在画布上,只是停止后续生成:

typescript 复制代码
for (let i = 0; i < plan.subtasks.length; i++) {
  if (abortSignal?.aborted) break  // 用户点了停止按钮
  const result = await executeSubAgent(...)
}

七、流式超时与心跳

与 LLM 的长连接需要精心的超时管理:

typescript 复制代码
export interface StreamTimeoutConfig {
  hardTimeoutMs: number       // 总超时(墙钟时间)
  noTextTimeoutMs: number     // 无活动超时(收到任何内容就重置)
  firstTextTimeoutMs?: number // 等待第一个 token 的超时
  thinkingResetsTimeout: boolean  // thinking token 是否重置超时
  pingResetsTimeout?: boolean     // 心跳是否重置超时
}

服务端每 15 秒发送一次 keep-alive ping,防止 SSE 连接被中间代理切断:

typescript 复制代码
// server/api/ai/chat.ts
const keepAlive = setInterval(() => {
  writer.write(`data: ${JSON.stringify({ type: 'ping' })}\n\n`)
}, 15000)

超时时长根据 prompt 长度动态调整------简短的 "login page" 不需要和复杂的 "电商平台首页" 用同样的超时:

typescript 复制代码
export function getSubAgentTimeouts(promptLength: number): StreamTimeoutConfig {
  if (promptLength < 200) return { hardTimeoutMs: 60_000, noTextTimeoutMs: 25_000, ... }
  if (promptLength < 500) return { hardTimeoutMs: 90_000, noTextTimeoutMs: 30_000, ... }
  return { hardTimeoutMs: 120_000, noTextTimeoutMs: 40_000, ... }
}

八、设计原则与经验总结

1. Orchestrator 要"轻"

编排 Agent 只做分解,不做任何设计决策。它的输出是结构化的 JSON Plan,而非自然语言。这使得解析稳定、执行确定。

2. 隔离胜过协调

相比让多个 Agent "互相通信",完全隔离 + ID 命名空间是更可靠的并行策略。每个 Agent 看到全局结构(知道自己是哪个区域),但无法影响其他 Agent 的输出。

3. 流式优先,批量兜底

用户体验的关键在于感知速度。即使总时间没变,逐个节点出现比等 30 秒后"砰"一下全出要好得多。但流式解析必须有批量回退兜底,因为 LLM 输出格式不总是完美的。

4. 渐进式高度扩展

根 Frame 的高度在生成过程中只会增长,不会缩短。这避免了"画布抖动"------内容一多一少地跳来跳去。只在所有 Agent 完成后才做最终的高度调整。

5. 给 Agent 一个身份

这看起来是个小功能,但 Agent Identity(颜色 + 名字)极大提升了用户对并行过程的理解和信任感。用户能看到 "Kiki 正在画导航栏"、"Mochi 在画 Hero",而不是一堆节点不知道从哪冒出来。

九、架构全景

flowchart TD Input["用户输入
"设计一个 SaaS 产品的 Landing Page""] Classify["意图分类 classifyIntent
DESIGN / CHAT / MODIFY"] Entry["executeOrchestration()"] Input --> Classify Classify -->|DESIGN| Entry subgraph Phase1["Phase 1: Planning"] Orch["Orchestrator Agent
callOrchestrator()
输出: OrchestratorPlan
(rootFrame + styleGuide + subtasks)"] end subgraph Phase2["Phase 2: Canvas Setup"] Setup["创建根 Frame + Agent 身份"] end subgraph Phase3["Phase 3: Sub-Agent 执行"] Agents["executeSubAgents()"] NavAgent["Nav Agent"] HeroAgent["Hero Agent"] FeatAgent["Feat Agent"] Insert["insertStreamingNode()
markNodesForAnimation()"] Agents --> NavAgent & HeroAgent & FeatAgent NavAgent -->|JSONL| Insert HeroAgent -->|JSONL| Insert FeatAgent -->|JSONL| Insert end subgraph Phase4["Phase 4: 后处理"] Post["adjustRootFrameHeight()
applyTreeHeuristics()"] end subgraph Phase5["Phase 5: 视觉校验"] Valid["runPostGenerationValidation()
Vision API 校验"] end Entry --> Phase1 Phase1 --> Phase2 Phase2 --> Phase3 Phase3 --> Phase4 Phase4 --> Phase5

十、总结

实现一个 Agent Team 系统,核心挑战不在于"调 API",而在于:

  1. 任务分解的质量 --- Orchestrator 的 prompt 决定了整个管线的上限
  2. 并发安全 --- ID 隔离 + 信号量,比锁更适合前端场景
  3. 流式体验 --- JSONL 增量解析 + 实时画布渲染 + 动画
  4. 优雅降级 --- 每一层都有 fallback,部分失败不影响整体
  5. 可观测性 --- Agent Identity + 进度 Checklist,让黑盒变透明

这套架构已经在 OpenPencil 中稳定运行,支持 1-6x 并发度,能在几秒内生成包含 100+ 节点的复杂 UI 设计。

如果你对设计工具、AI 生成或多 Agent 系统感兴趣,欢迎查看 OpenPencil 的源码,所有代码都是开源的。


关于 OpenPencil

OpenPencil 是全球首个开源 AI 原生矢量设计工具,也是业界首个实现并发 Agent Team 协作生成的设计工具。

核心特性:

  • AI 原生设计 --- 输入一句话,多个 AI Agent 协作在画布上实时生成完整 UI,支持 1-6x 并发
  • Design-as-Code --- 设计即代码,一键导出 React + Tailwind / HTML + CSS,设计变量自动生成 CSS Variables
  • 专业矢量编辑 --- 基于 Fabric.js v7,支持 Frame、Auto Layout、布尔运算、钢笔工具、智能参考线等专业功能
  • 设计变量系统 --- 完整的 Design Token 管理,支持多主题轴(Light/Dark、Compact/Comfortable)
  • Figma 导入 --- 直接解析 .fig 文件,无需通过 API
  • MCP Server --- 内置 MCP 服务器,可被 Claude Code、Cursor 等 AI 工具直接调用
  • 全平台桌面端 --- 基于 Electron,支持 macOS、Windows、Linux,含自动更新

如果觉得有用,欢迎去 GitHub 点个 Star 支持一下!

相关推荐
swipe2 小时前
深入理解 JavaScript 中的 this 绑定机制:从原理到实战
前端·javascript·面试
Json_Lee2 小时前
2026 年了,多 Agent 编码该怎么选?agent-team vs Claude Agent Teams vs Claude Squad vs Met
前端·后端·vibecoding
Novlan12 小时前
Stepper 小数输入精度丢失 Bug 修复
前端
陈随易2 小时前
刚上市就断货?如此火爆的编程显示器到底有什么魔力
前端·后端·程序员
数据智能老司机2 小时前
AI 智能体与应用——使用 LangGraph 构建基于工具的智能体
llm·agent
兆子龙2 小时前
前端哨兵模式(Sentinel Pattern):优雅实现无限滚动加载
前端·javascript·算法
豆苗学前端2 小时前
彻底讲透浏览器渲染原理,吊打面试官
前端·javascript·面试
数据智能老司机3 小时前
AI 智能体与应用——问题转换
llm·agent
踩着两条虫3 小时前
AI 驱动的 Vue3 应用开发平台 入门指南(五):创建 H5 移动应用
前端·vue.js·ai编程