从手写 Runner 到 LangGraph:受控 Agent 接入 LangGraph

本文基于 AI Mind 真实实现整理。

GitHub:github.com/HWYD/ai-min... 对应版本:v0.2.0

AI Mind 是一个正在持续升级的 Next.js AI Chat 项目。它从本地聊天开始,逐步加入流式协议、工具调用(Tool Calling)、Skill、MCP、受控 Agent 和 Agent 图编排能力(Agent Graph)。

如果你对这个项目感兴趣,或者这篇文章对你有一点帮助,也欢迎顺手到 GitHub 帮 AI Mind 点个 Star⭐,这会是对我继续更新很大的鼓励。

这篇文章复盘的是:我如何把一个已经受控的任务清单 Agent(Tasklist Agent),从手写 Runner / 状态机流程编排,迁移到 LangGraph StateGraph(状态图 / 可执行流程图)。

一提到 LangGraph,很多人第一反应可能是:

  • 是不是要做更复杂的 Agent?
  • 是不是要让模型自己决定下一步?
  • 是不是要开放更多工具调用和自动规划?

但在 AI Mind 的 v0.2.0 里,我接入 LangGraph,并不是为了让 Agent 更自由。

恰恰相反,这一版的目标是:

在不扩大 Agent 权限的前提下,把一条已经受控的 Agent 主链路,从手写 Runner 迁移成更显式、更可观察的图编排。

所以这不是一篇"LangGraph 怎么用"的教程,而是一篇真实项目里的工程迁移复盘。

它真正讨论的是:

text 复制代码
以前:
手写 Runner 里维护顺序调用、if / else 分支、状态推进和提前停止

现在:
用 LangGraph 的 StateGraph、node(节点)、edge(边)、conditional edge(条件边)表达同一条受控路径

换句话说,v0.2.0 不是让 Tasklist Agent 更开放,而是让它的执行路径更清楚。


1. 先说清楚:Tasklist Agent 到底是做什么的

如果第一次看到 AI Mind,可能会先疑惑:

  • Tasklist Agent 是什么?
  • 它到底在帮谁做什么?

在 AI Mind 里,Tasklist Agent(任务清单 Agent)的作用很具体:

用户给它一个版本方案文档,它根据这个方案生成一份可执行的开发 tasklist(任务清单)草稿。

也就是说,它不是从一句"帮我规划一个版本"开始自由发挥,而是必须基于用户显式引用的版本方案工作。

它的输入大概是:

text 复制代码
/tasklist + @docs://versions/*.md

它的输出不是直接写文件,而是通过 AgentTextArtifactPanel(产物展示面板)展示一份可复制的 tasklist Markdown。

可以简单理解成:

text 复制代码
版本方案文档
  -> Agent 读取
  -> 生成开发任务清单
  -> 校验 tasklist 结构
  -> 必要时最多修正一次
  -> 输出最终 Artifact(产物)

这个场景很适合做成 Agent,但也很容易失控。

如果不设边界,模型可能会:

  • 自己扫描版本目录;
  • 读取未授权的文档;
  • 根据一句裸目标自由生成 tasklist;
  • 无限补上下文;
  • 无限修正;
  • 直接写入 docs 文件。

所以 AI Mind 里的 Tasklist Agent 一开始就不是开放式 Agent,而是受控 Agent。

这也是为什么这篇文章反复强调:

LangGraph 接入的是一条已经受控的 Agent 主路径,而不是给模型开放更多自由。


2. 这里的 Runner 是什么

标题里有一个词:Runner。

这里的 Runner(执行器)不需要理解成一个复杂框架概念,可以先理解为:一段负责"按固定顺序推进 Agent 步骤"的执行器。

v0.1.1 里,这个手写 Runner 大概负责依次推进这些事情:

text 复制代码
读取版本方案
判断方案是否足够生成 tasklist
做一次受控规划决策
必要时读取一个可选上下文
生成 tasklist 拆分策略
生成 tasklist 草稿 v1
执行结构校验
必要时最多修正一次
再次校验 v2
评估修正效果
输出最终 Artifact

所以,"从手写 Runner 到 LangGraph"的意思不是把业务规则重写掉,也不是把 Agent 变成自由规划器。

它更准确的含义是:

把原来由手写执行器推进的顺序调用和 if / else 分支,迁移成 LangGraph 的 StateGraph、node(节点)和 conditional edge(条件边)。

这里顺便区分一下几个词:

text 复制代码
Runtime(运行时):
更大的运行时边界,负责入口判断、资源边界、工具权限、状态机约束、限制条件和失败收束。

Runner(执行器):
某一种具体执行实现,负责把一轮 Agent 步骤按顺序推进完。

Graph / StateGraph(图编排 / 状态图):
新的编排表达方式,用 node(节点)和 conditional edge(条件边)描述 Runner 原本要执行的流程。

所以这次迁移不是"从 Runtime 迁移到 LangGraph"。

而是:

把 Tasklist Agent 的 Runner 编排迁移到 LangGraph。


3. v0.1.1 的 Tasklist Agent 已经受控到什么程度

v0.2.0 之前,AI Mind 已经有了一个受控 Tasklist Agent。

它的入口非常明确:

text 复制代码
/tasklist + @docs://versions/*.md

也就是说,用户必须显式选择 /tasklist,并且显式引用一个 docs://versions/*.md 版本方案,Agent 才会进入 tasklist 生成链路。

如果用户只说"帮我规划一个版本",但没有引用版本方案,Agent 不会偷偷扫描 docs/versions,也不会从自然语言目标直接生成 tasklist。

这条主链路大概是:

text 复制代码
用户显式选择 /tasklist
  -> 用户显式引用 docs://versions/*.md
  -> Runtime 读取 version plan
  -> Runtime 生成 planExtract
  -> 规则判断方案是否足够生成 tasklist
  -> 模型只在白名单 action 中做一次规划决策
  -> 可选读取 1 个白名单上下文
  -> 生成 tasklist 拆分策略
  -> 生成 tasklistDraft v1
  -> validate_tasklist_structure 确定性校验
  -> 判断 warning 是否需要修正
  -> 最多修正一次生成 tasklistDraft v2
  -> v2 再次校验,但不生成 v3
  -> 评估修正效果
  -> Agent Text Artifact 展示最终 tasklist Markdown

这里有几个关键边界。

第一,模型不是自由规划。它只能在几个白名单动作中选择一次下一步方向。

第二,Agent 不是自由读取资源。version plan 必须由用户显式引用,可选上下文也只能读取白名单资源,而且最多一次。

第三,Agent 不是无限修正。tasklistDraft 最多从 v1 修正到 v2,v2 再校验,但不允许生成 v3。

第四,Agent 不写文件。最终 tasklist 通过 AgentTextArtifactPanel 展示,用户可以复制,但 Agent 不会写入 docs/tasklists/*

所以,v0.2.0 不是从零开始做 Agent。

它是在一条已经受控的 Agent 主链路上,继续演进编排层。


4. 先简单理解 LangGraph:它解决的是 Agent 流程编排问题

如果第一次接触 LangGraph,可以先把它理解成:

用图来组织 Agent / LLM 应用执行流程的编排工具。

在本文里,我们只需要理解几个概念,不需要先掌握 LangGraph 的全部 API。

4.1 StateGraph:一张可以执行的流程图

StateGraph 可以理解成一张"可执行流程图"。

普通流程图只能看,StateGraph 里的 node(节点)可以执行逻辑,node 之间通过 edge(边)连接,执行时会带着一份状态对象(state)一路往下走。

对应到 AI Mind 这个 Tasklist Agent,流程大概是:

text 复制代码
读取版本方案
  -> 评估方案就绪度
  -> 做一次规划决策
  -> 生成 tasklist 草稿 v1
  -> 校验 tasklist v1
  -> 输出最终 Artifact

在手写 Runner 里,这些可能是一串函数调用。

迁移到 StateGraph 之后,它们就变成了一个个有名字的 node(节点)。

4.2 node(节点):每一步具体做什么

node(节点)就是图里的一个执行节点。

它接收当前状态,执行一段逻辑,然后返回一段 state patch(状态补丁)。

在 AI Mind 里,node 不是随便新写一套业务逻辑,而是复用已有的 Step Operation(可复用业务步骤)。

比如:

text 复制代码
读取版本方案节点
规划决策节点
生成 tasklist 草稿 v1 节点
校验 tasklist v1 节点
修正 tasklist 草稿 v2 节点
输出最终 Artifact 节点

这些 node 本质上仍然做原来那些事,只是被放进了图编排里。

所以在这篇文章里,node 可以理解成:

把原来 Runner 里的某一步,变成图里的一个命名执行节点。

4.3 edge(边)和 conditional edge(条件边):下一步怎么走

edge(边)表示固定顺序。

比如:

text 复制代码
读取版本方案节点 -> 评估方案就绪度节点

conditional edge(条件边)表示条件分支。

比如规划决策节点之后,可能走向:

text 复制代码
追问澄清
边界停止
读取可选上下文
决定 tasklist 拆分策略

这正好对应 AI Mind 这版最重要的迁移点:

把手写 Runner 里的 if / else 分支,迁移成可命名、可测试、可观察的 route(路由)。

注意,这不是让模型自由选择下一步。

模型仍然只能输出白名单动作,Graph 只是根据这个受控结果走到对应 node

4.4 State / state patch:中间状态怎么传递

Graph 执行时会带着一份状态对象(state)。

每个 node 不一定返回完整状态,而是返回一段 state patch(状态补丁),再由 reducer(合并规则)合并回 GraphState。

在 AI Mind 里,我没有用 GraphState 替代原来的 AgentState。

更准确的关系是:

text 复制代码
AgentState:Agent 业务状态,仍然是业务事实源
GraphState:图执行状态,包住 AgentState,并记录 Graph 执行轨迹

也就是说,Agent 当前读到了哪个 version plan、生成了什么 tasklist draft、校验结果是什么,这些业务事实仍然在原来的 AgentState 里。

GraphState 主要负责多记录一些图执行相关信息:

text 复制代码
threadId
currentNode
visitedNodes
routes
statePatchSummaries

4.5 reducer(合并规则):state patch 怎么合并

既然 node 返回的是局部状态更新,那不同字段怎么合并就很重要。

比如:

text 复制代码
currentNode:应该替换成最新节点
visitedNodes:应该追加 nodeId
routes:应该追加 route entry
statePatchSummaries:应该追加状态变更摘要

如果这里不定义清楚,后面的 node 更新可能会把前面的轨迹覆盖掉。

所以在 v0.2.0 里,我明确给 GraphState 的不同字段设计了 replace / append 语义。

这一点不是很炫,但很重要。

因为 Graph Trace 能不能稳定展示,依赖这些轨迹字段不会被后续状态补丁覆盖。


5. 为什么 v0.1.1 暂时不接 LangGraph,v0.2.0 才接

我没有一开始做 Agent 时就接 LangGraph。

这是一个有意为之的选择。

v0.1.0v0.1.1,最重要的问题不是"用什么框架编排",而是先验证受控 Agent 本身能不能成立。

当时要先收住这些边界:

  • 入口是不是明确?
  • 资源读取会不会越界?
  • 模型能不能自由选择任意动作?
  • 可选上下文会不会无限读?
  • warning 会不会无限修?
  • tasklist 会不会写入 docs?
  • 最终产物能不能稳定通过 Artifact 展示?
  • 普通问答、docs summary、工具调用会不会被影响?

如果这些边界还没有跑稳,过早接 LangGraph,可能只是把不稳定流程换成另一种框架写法。

Graph 不能替代边界设计。

它能让流程显式,但不能自动让 Agent 安全。

所以我的判断是:

边界没稳时,Graph 只是更复杂的表达;边界稳定后,Graph 才能变成更合适的编排层。

到了 v0.2.0,Tasklist Agent 的受控路径已经比较清楚:

  • 入口固定;
  • 动作白名单固定;
  • 可选上下文限制固定;
  • tasklist 生成和校验链路固定;
  • v2 后不再修正;
  • Artifact 输出语义固定。

这时再引入 LangGraph,就不是为了追框架,而是为了让一条稳定流程变得更显式、更可观察。


6. 手写 Runner 的问题:不是不能用,而是不够显式

手写 Runner 本身没有问题。

v0.1.0v0.1.1,手写 Runner 反而是更合适的。

因为那个阶段流程还在变,直接用函数顺序调用、状态机约束和少量分支判断,能更快验证主链路。

但当流程里同时出现这些分支时,手写 Runner 的复杂度就开始变明显:

text 复制代码
提前停止
可选上下文
warning 分流
最多一次修正
最终 Artifact 输出

这时,Runner 里的 if / else 会越来越像一张隐形流程图。

流程大概会经过这些步骤:

text 复制代码
读取版本方案
评估方案就绪度
做一次规划决策
读取可选上下文
决定 tasklist 拆分策略
生成 tasklist 草稿 v1
校验 tasklist v1
判断 warning 处理方式
修正 tasklist 草稿 v2
校验 tasklist v2
评估修正效果
输出最终 Artifact
追问澄清 / 边界停止

如果继续放在手写 Runner 里,它会变成:

text 复制代码
一串顺序调用
+ 一些 if / else
+ 一些提前 stop
+ 一些 warning 分流
+ 一些 revision 分支

这种写法仍然能跑,但读代码的人需要在脑子里还原流程图。

而且后续如果要做:

  • node 级执行过程;
  • 路由摘要;
  • 状态变更摘要;
  • checkpoint(检查点);
  • 调试摘要;
  • interrupt(中断)、HITL(人工介入)、replay(重放)的架构入口;

手写 Runner 就会越来越吃力。

所以这版我更愿意把它表达成 Graph。

text 复制代码
v0.1.1 手写 Runner
  -> if / else
  -> 顺序调用
  -> 状态机约束
  -> agent-step 执行过程

v0.2.0 StateGraph
  -> node(节点)
  -> conditional edge(条件边)
  -> state patch(状态补丁)
  -> Graph 事件
flowchart LR subgraph Runner[&#34;v0.1.1 手写 Runner&#34;] R1[&#34;顺序调用&#34;] R2[&#34;if / else 分支&#34;] R3[&#34;状态机约束&#34;] R4[&#34;agent-step 执行过程&#34;] end subgraph Shared[&#34;共享层:业务规则和边界不变&#34;] S1[&#34;Step Operation<br/>共享业务步骤&#34;] S2[&#34;Runtime Guard<br/>入口 / 资源 / 工具 / 次数限制&#34;] S3[&#34;Final Artifact<br/>最终产物输出&#34;] end subgraph Graph[&#34;v0.2.0 StateGraph&#34;] G1[&#34;node(节点)&#34;] G2[&#34;conditional edge(条件边)&#34;] G3[&#34;state patch(状态补丁)&#34;] G4[&#34;Graph 事件&#34;] end R2 --> S1 R3 --> S2 R4 --> S3 S1 --> G1 S2 --> G2 S3 --> G4

这里的关键不是"手写 Runner 被淘汰了"。

而是:

手写 Runner 不是错,只是当分支变多、路径变长、后续还要做执行过程展示、调试摘要和检查点时,显式 Graph 更适合承载这条链路。


7. LangGraph 在这一版只替换编排层

这是这篇文章最重要的一点。

v0.2.0 使用 LangGraph,但只替换 Agent 编排层。

LangGraph 接管的是:

text 复制代码
node(节点)编排
edge(边)编排
conditional edge(条件边)路由
GraphState 的局部更新
Graph 事件输出

LangGraph 不接管的是:

text 复制代码
Agent 入口边界
资源白名单
工具权限范围
最大步骤数 maxSteps
最多一次可选上下文读取 maxOptionalContextReads
最多一次草稿修正 maxDraftRevisions
stopped(已停止)状态边界
v2 后禁止继续修正
最终输出前必须完成校验和修正效果评估
文件写入权限
源码读取权限
自由工具调用

这意味着:

StateGraph 决定"流程怎么走",Runtime guard(运行时守卫)决定"Agent 能不能做"。

比如,Graph 里可以有"读取可选上下文节点"。

但这个节点不能随便读任何资源。

它仍然必须满足:

text 复制代码
规划决策动作是 read_optional_context
只能读取 decision 指定的白名单 URI
最多读取一次
读取失败后降级继续,并加入人工复核点

再比如,Graph 里可以有"修正 tasklist 草稿 v2 节点"。

但它仍然不能无限循环修正:

text 复制代码
最多自动修正一次
v2 必须再次校验
v2 后不生成 v3

所以,这版接入 LangGraph 后,Agent 并没有变得更自由。

它只是把受控路径表达成了 Graph。


8. 迁移前先做一件事:抽共享 Step Operation

在真正写 Graph Runner 之前,我先做了一件很重要的事:

把原来 legacy runner(旧版执行器)里的业务步骤抽成共享 Step Operation。

这里的 Step Operation 可以理解成"单个业务步骤的可复用执行函数"。比如规划决策、生成草稿、结构校验、修正草稿这些动作,都先从旧版 Runner 里抽出来,变成 legacy runner 和 graph runner 都能复用的步骤。

原因很简单:我不希望为了接入 LangGraph,复制一套业务流程。

如果复制一套,后面会出现几个问题:

  • legacy runner 和 graph runner 行为可能漂移;
  • 修改 bug 时要改两套;
  • 行为等价测试会变复杂;
  • graph runner 可能绕过原来的状态机约束;
  • 受控边界可能出现分叉。

所以正确方向是:

text 复制代码
legacy runner(旧版执行器)
       \
        -> shared step operations(共享业务步骤)
       /
graph runner(图编排执行器)

我没有让 graph runner 重新实现一套读取、决策、生成、校验、修正逻辑。

真正稳妥的迁移方式是:

先把业务步骤抽成共享 Step Operation,再让 legacy runner 和 graph runner 只是"编排方式不同"。

这些 Step Operation 包括:

text 复制代码
方案就绪度判断
规划决策
tasklist 拆分策略
生成 tasklist 草稿
warning 处理方式判断
修正效果评估
修正 tasklist 草稿

对应到代码里,就是类似这些共享函数:

text 复制代码
runPlanReadinessStep
runPlanningDecisionStep
runTasklistStrategyStep
runDraftTasklistStep
runWarningDispositionStep
runRevisionEffectStep
runReviseTasklistStep

它们继续复用原来的:

text 复制代码
readiness rule
planner schema
optional context whitelist
TasklistStrategy prompt builder
draft / revise prompt builder
validate_tasklist_structure
warning disposition
revision effect
final artifact emitter
state-machine guard
limits

这样一来,legacy runner 和 graph runner 的区别主要在"怎么编排",不是"业务规则是否一致"。

这点对迁移很关键。

因为真正危险的不是接入 LangGraph,而是为了接入 LangGraph 复制一套业务流程。

ts 复制代码
export async function runPlanningDecisionStep(
    options: TasklistAgentStepOperationOptions
): Promise<{ output: PlanningDecisionOutput; state: VersionPlanTasklistAgentState }> {
    const stepIndex = getNextStepIndex(options.state)
    const step = startAgentStep({
        actionType: 'planning_decision',
        state: options.state,
        stepIndex,
        title: '执行规划决策',
        writeChunk: options.writeChunk,
    })

    const output = await generatePlanningDecisionOutput(
        options.model,
        options.state,
        options.userGoal,
        options.context.signal
    )

    const nextState = applyVersionPlanTasklistAgentAction(options.state, {
        type: 'planning_decision',
        decision: output.decision,
        reason: output.decision.reason,
    })

    endAgentStep({
        actionType: 'planning_decision',
        durationStartedAt: step.startedAt,
        partId: step.partId,
        severity:
            output.decision.type === 'ask_clarification' ||
            output.decision.type === 'stop_with_boundary_message'
                ? 'warning'
                : 'info',
        state: nextState,
        stepIndex,
        summary: getPlanningDecisionSummary(output.decision),
        tags: [`action: ${output.decision.type}`],
        title: '执行规划决策',
        writeChunk: options.writeChunk,
    })

    return { output, state: nextState }
}

这里保留的是 runPlanningDecisionStep 的关键路径:开始 step、生成受控规划决策、通过状态机 action 推进 AgentState、结束 step 并返回新状态。runWarningDispositionSteprunDraftTasklistSteprunReviseTasklistStep 等共享步骤也是类似模式,只是内部业务动作不同。


9. GraphState 设计:不替代 AgentState

接入 Graph 后,很容易产生一个冲动:

  • 既然用了 LangGraph,是不是应该重新设计一份 GraphState,把所有业务状态都平铺进去?

我这版没有这么做。

原因是:v0.1.1VersionPlanTasklistAgentState 已经表达了业务事实,而且状态机约束也是围绕它工作的。

如果这时为了 LangGraph 重写一份平铺 GraphState,迁移风险会很高。

所以我采用的是包裹式设计:

text 复制代码
VersionPlanTasklistGraphState
  ├─ agentState
  ├─ threadId
  ├─ input
  ├─ graph
  └─ output

其中:

text 复制代码
agentState:仍然是业务事实源
graph:只记录图执行轨迹

Graph runtime state 大概记录这些内容:

text 复制代码
runtimeMode
checkpointMode
currentNode
visitedNodes
routes
lastRoute
statePatchSummaries

也就是说:

AgentState 记录"当前任务事实",GraphState 记录"Graph 怎么执行"。

这也带来一个安全边界。

GraphState 在服务端运行时内部可以持有完整 AgentState。

但 stream event(流式事件)、调试摘要、UI view model(界面展示模型)和日志,不能暴露完整业务事实。

比如不能把这些内容直接发到前端:

text 复制代码
完整 prompt
完整 model messages
完整 version plan 原文
完整 optional context 原文
完整 tasklist draft
完整 tool raw output
完整 GraphState
完整 AgentState

前端真正需要的是:

text 复制代码
当前跑到哪个 node(节点)
走了哪条 route(路由)
发生了什么状态摘要
是否有 warning
是否进入修正
最终 artifact 在哪里

这也是后面 Graph 事件和 AgentTracePanel(执行过程面板)的基础。

ts 复制代码
export interface VersionPlanTasklistGraphRuntimeState {
    checkpointMode: TasklistAgentRuntimeConfig['graphCheckpointMode']
    currentNode?: string
    lastRoute?: VersionPlanTasklistGraphRoute
    routes: VersionPlanTasklistGraphRoute[]
    runtimeMode: 'graph'
    // 这里只保存可展示的状态变化摘要,不写入完整 prompt、资源正文、草稿正文或 tool output。
    statePatchSummaries: VersionPlanTasklistGraphStatePatchSummary[]
    visitedNodes: string[]
}

export interface VersionPlanTasklistGraphState {
    // GraphState 只包裹现有 AgentState;业务事实源仍然由受控状态机维护。
    agentState: VersionPlanTasklistAgentState
    graph: VersionPlanTasklistGraphRuntimeState
    input: VersionPlanTasklistGraphInput
    output?: VersionPlanTasklistGraphOutput
    threadId: string
}

export function reduceVersionPlanTasklistGraphRuntimeState(
    left: VersionPlanTasklistGraphRuntimeState,
    right: VersionPlanTasklistGraphRuntimeStateUpdate
): VersionPlanTasklistGraphRuntimeState {
    return {
        checkpointMode: right.checkpointMode ?? left.checkpointMode,
        currentNode: right.currentNode ?? left.currentNode,
        lastRoute: right.lastRoute ?? left.lastRoute,
        runtimeMode: 'graph',

        // LangGraph node 返回 partial update;轨迹数组必须累加,避免节点历史被覆盖。
        routes: [...left.routes, ...(right.routes ?? [])],
        statePatchSummaries: [...left.statePatchSummaries, ...(right.statePatchSummaries ?? [])],
        visitedNodes: [...left.visitedNodes, ...(right.visitedNodes ?? [])],
    }
}

完整实现里还会通过 Annotation.Root 把这些 reducer 注册给 LangGraph。博客这里只保留状态结构和合并规则,因为它们最能说明"GraphState 只是包裹 AgentState,并记录图执行轨迹"。


10. 把主流程映射成 StateGraph

真正迁移时,核心就是把原来的主链路映射成 StateGraph 的 node(节点)和 conditional edge(条件边)。

大致结构是这样:

flowchart TD Start([&#34;START&#34;]) --> Read[&#34;读取版本方案<br/>readVersionPlan&#34;] Read --> Ready[&#34;评估方案就绪度<br/>evaluatePlanReadiness&#34;] Ready --> Decision[&#34;规划决策<br/>planningDecision&#34;] Decision --> RouteDecision{&#34;规划决策后的条件路由<br/>routeAfterPlanningDecision&#34;} RouteDecision -- &#34;ask_clarification&#34; --> Ask[&#34;追问澄清<br/>askClarification&#34;] Ask --> EndAsk([&#34;END&#34;]) RouteDecision -- &#34;stop_with_boundary_message&#34; --> Stop[&#34;边界停止<br/>stopWithBoundaryMessage&#34;] Stop --> EndStop([&#34;END&#34;]) RouteDecision -- &#34;read_optional_context&#34; --> Optional[&#34;读取可选上下文<br/>readOptionalContext&#34;] Optional --> Strategy[&#34;决定 tasklist 拆分策略<br/>decideTasklistStrategy&#34;] RouteDecision -- &#34;proceed / manual_review&#34; --> Strategy Strategy --> Draft[&#34;生成 tasklist 草稿 v1<br/>draftTasklistV1&#34;] Draft --> Validate1[&#34;校验 tasklist v1<br/>validateTasklistV1&#34;] Validate1 --> Warning[&#34;判断 warning 处理方式<br/>decideWarningDisposition&#34;] Warning --> RouteWarning{&#34;warning 处理后的条件路由<br/>routeAfterWarningDisposition&#34;} RouteWarning -- &#34;fixNow > 0&#34; --> Revise[&#34;修正 tasklist 草稿 v2<br/>reviseTasklistV2&#34;] Revise --> Validate2[&#34;校验 tasklist v2<br/>validateTasklistV2&#34;] Validate2 --> Revision[&#34;评估修正效果<br/>evaluateRevisionEffect&#34;] RouteWarning -- &#34;no auto revision&#34; --> Revision Revision --> Artifact[&#34;输出最终 Artifact<br/>emitFinalArtifact&#34;] Artifact --> EndFinal([&#34;END&#34;])

这里有几个点很重要。

第一,规划决策后会进入 conditional edge(条件边)。

它根据规划决策动作路由到不同 node。

第二,warning 处理后也会进入 conditional edge(条件边)。

它根据 warning 处理结果判断是否进入"修正 tasklist 草稿 v2"。

第三,追问澄清和边界停止是提前结束路径。

它们不会继续生成 tasklist draft。

第四,非修正分支也必须经过"评估修正效果"。

因为最终输出守卫要求状态到达 revision_effect_evaluated

第五,最终 tasklist 仍然通过 Agent Text Artifact 展示。

Graph 不改变最终产物交付方式。

ts 复制代码
export function createVersionPlanTasklistGraph(options: CreateVersionPlanTasklistGraphOptions) {
    const nodeIds = VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS
    const withEvents = (nodeId: VersionPlanTasklistGraphNodeId, node: GraphNodeHandler) =>
        withGraphNodeEvents(nodeId, node, options.runtime)

    return new StateGraph(VersionPlanTasklistGraphStateAnnotation)
        .addNode(nodeIds.readVersionPlan, withEvents(nodeIds.readVersionPlan, createReadVersionPlanNode(options.runtime)))
        .addNode(nodeIds.planningDecision, withEvents(nodeIds.planningDecision, createPlanningDecisionNode(options.runtime)))
        .addNode(nodeIds.decideTasklistStrategy, withEvents(nodeIds.decideTasklistStrategy, createDecideTasklistStrategyNode(options.runtime)))
        .addNode(nodeIds.draftTasklistV1, withEvents(nodeIds.draftTasklistV1, createDraftTasklistV1Node(options.runtime)))
        .addNode(nodeIds.validateTasklistV1, withEvents(nodeIds.validateTasklistV1, createValidateTasklistV1Node(options.runtime)))
        .addNode(nodeIds.decideWarningDisposition, withEvents(nodeIds.decideWarningDisposition, createDecideWarningDispositionNode(options.runtime)))
        .addNode(nodeIds.emitFinalArtifact, withEvents(nodeIds.emitFinalArtifact, createEmitFinalArtifactNode(options.runtime)))

        .addEdge(START, nodeIds.readVersionPlan)
        .addEdge(nodeIds.draftTasklistV1, nodeIds.validateTasklistV1)
        .addEdge(nodeIds.validateTasklistV1, nodeIds.decideWarningDisposition)

        .addConditionalEdges(nodeIds.planningDecision, routeAfterPlanningDecision, [
            nodeIds.askClarification,
            nodeIds.stopWithBoundaryMessage,
            nodeIds.readOptionalContext,
            nodeIds.decideTasklistStrategy,
            END,
        ])
        .addConditionalEdges(nodeIds.decideWarningDisposition, routeAfterWarningDisposition, [
            nodeIds.reviseTasklistV2,
            nodeIds.evaluateRevisionEffect,
        ])

        // 其他节点和边省略:readOptionalContext / reviseTasklistV2 / validateTasklistV2 / evaluateRevisionEffect ...
        .compile({
            checkpointer: options.checkpointer,
            name: options.graphName ?? 'version-plan-tasklist-agent-graph',
        })
}

这段只保留了主干。完整实现里还有可选上下文读取、v2 修正、v2 校验、修正效果评估、追问澄清和边界停止等节点。


11. Conditional Edge:把关键 if / else 变成命名路由

这版里最值得讲的 LangGraph 点,不是 API 本身,而是 conditional edge(条件边)。

因为 Tasklist Agent 里有两个很典型的受控分支。

第一个是规划决策后的分支。

模型只能在几个白名单动作里选一次:

text 复制代码
ask_clarification
stop_with_boundary_message
read_optional_context
proceed_to_tasklist_strategy
proceed_with_manual_review_items

映射成 Graph 的路由后,大概是:

text 复制代码
ask_clarification -> 追问澄清节点
stop_with_boundary_message -> 边界停止节点
read_optional_context -> 读取可选上下文节点
proceed_to_tasklist_strategy -> 决定 tasklist 拆分策略节点
proceed_with_manual_review_items -> 决定 tasklist 拆分策略节点

第二个是 warning 处理后的分支。

tasklistDraft v1 校验后,会判断 warning 应该自动修,还是交给人工复核:

text 复制代码
fixNow.length > 0 -> 修正 tasklist 草稿 v2
fixNow.length === 0 -> 评估修正效果

这里最重要的是:conditional edge(条件边)并不是让模型自由决定下一步。

模型输出仍然受 schema(结构约束)和白名单约束。

Graph 只是把"受控分支"显式表达出来。

所以我对 conditional edge(条件边)的理解是:

它的价值不是放开规划,而是把已经受控的分支变成可命名、可测试、可观察的路由。

这比手写 if / else 更适合后续执行过程展示和调试摘要。

ts 复制代码
export function getRouteAfterPlanningDecision(
    state: VersionPlanTasklistGraphStateAnnotationState
): VersionPlanTasklistGraphRoute {
    if (state.output?.status === 'stopped') {
        return {
            fromNodeId: VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.planningDecision,
            label: 'controlled_output_failed',
            reason: state.output.textSummary ?? '规划决策输出不符合受控 JSON schema,本轮已安全停止。',
            toNodeId: END,
        }
    }

    const decision = state.agentState.artifacts.planning.decision

    if (!decision) {
        throw new Error('缺少 PlanningDecisionAction,无法决定 graph 下一跳。')
    }

    switch (decision.type) {
        case 'ask_clarification':
            return toRoute(decision, VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.askClarification)
        case 'stop_with_boundary_message':
            return toRoute(decision, VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.stopWithBoundaryMessage)
        case 'read_optional_context':
            return toRoute(decision, VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.readOptionalContext)
        case 'proceed_to_tasklist_strategy':
        case 'proceed_with_manual_review_items':
            return toRoute(decision, VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.decideTasklistStrategy)
        default:
            throw new Error('未知 PlanningDecisionAction,graph 已拒绝继续路由。')
    }
}

这里省略了 toRoute 的字段组装细节。关键是:每一种白名单动作都只能进入固定节点,未知动作直接拒绝继续路由。

ts 复制代码
export function getRouteAfterWarningDisposition(state: VersionPlanTasklistGraphStateAnnotationState): VersionPlanTasklistGraphRoute {
    const disposition = state.agentState.artifacts.planning.warningDisposition

    if (!disposition) {
        throw new Error('缺少 WarningDisposition,无法决定 graph 下一跳。')
    }

    const shouldRevise = disposition.fixNow.length > 0

    return {
        fromNodeId: VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.decideWarningDisposition,
        label: shouldRevise ? 'fix_now' : 'no_auto_revision',
        reason: disposition.reason,
        toNodeId: shouldRevise
            ? VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.reviseTasklistV2
            : VERSION_PLAN_TASKLIST_GRAPH_NODE_IDS.evaluateRevisionEffect,
    }
}

export function routeAfterWarningDisposition(state: VersionPlanTasklistGraphStateAnnotationState): WarningDispositionRouteTarget {
    return getRouteAfterWarningDisposition(state).toNodeId as WarningDispositionRouteTarget
}

12. 运行时配置:请求开始前选择 legacy 或 graph

迁移期间,我保留了 legacy runner(旧版执行器)。

但 legacy runner 和 graph runner 的选择,不是前端按钮,也不是用户功能。

它是服务端运行时配置(runtime config)。

大概是:

env 复制代码
AI_MIND_TASKLIST_AGENT_RUNTIME=graph
AI_MIND_GRAPH_EVENTS=on
AI_MIND_GRAPH_CHECKPOINT=off
AI_MIND_GRAPH_DEBUG_VIEW=off

如果需要回退,也可以配置成:

env 复制代码
AI_MIND_TASKLIST_AGENT_RUNTIME=legacy
AI_MIND_GRAPH_EVENTS=off
AI_MIND_GRAPH_CHECKPOINT=off
AI_MIND_GRAPH_DEBUG_VIEW=off

这里有几个边界。

第一,不做前端运行时切换按钮。

用户不应该关心这次 Agent 是手写 Runner 还是 LangGraph Runner。

第二,不做运行中回退。

也就是说,不会出现:

text 复制代码
graph runner 执行一半失败
  -> 自动切 legacy runner 接着跑

因为这样会带来很多问题:

  • Graph 事件已经发出;
  • 前端执行过程面板已经开始展示 Graph 时间线;
  • runId / threadId / partId / artifactId 已经生成;
  • 中途切换可能导致 UI 混乱;
  • 还可能重复模型调用或重复 Artifact。

所以回退只发生在请求开始前。

第三,配置只影响 Tasklist Agent。

普通问答、普通 docs summary、工具调用、Reader Skill、Utility Skill 主链路不受影响。

这一节的核心是:

Runtime selection(运行时选择)是工程配置,不是用户功能。

ts 复制代码
export function getTasklistAgentRuntimeConfig(
    env: RuntimeEnv = process.env,
    nodeEnv: string | undefined = process.env.NODE_ENV
): TasklistAgentRuntimeConfig {
    // 迁移期默认 legacy,只有显式开启 graph 时才进入新编排路径,避免半成品 graph 影响现有 /tasklist 链路。
    const runtimeMode: TasklistAgentRuntimeMode = env.AI_MIND_TASKLIST_AGENT_RUNTIME?.trim() === 'graph' ? 'graph' : 'legacy'
    const graphRuntimeEnabled = runtimeMode === 'graph'

    // Graph 附加能力只在 graph runtime 下生效;legacy fallback 不应该发送 graph events 或暴露 debug 摘要。
    const graphEventsEnabled = graphRuntimeEnabled && env.AI_MIND_GRAPH_EVENTS?.trim() === 'on'
    const graphDebugViewEnabled = graphRuntimeEnabled && env.AI_MIND_GRAPH_DEBUG_VIEW?.trim() === 'on'

    // memory checkpoint 只用于开发态调试,production 下强制 off,避免被误解为可恢复的产品级状态。
    const graphCheckpointMode: GraphCheckpointMode =
        graphRuntimeEnabled && env.AI_MIND_GRAPH_CHECKPOINT?.trim() === 'memory' && nodeEnv !== 'production' ? 'memory' : 'off'

    return {
        graphCheckpointMode,
        graphDebugViewEnabled,
        graphEventsEnabled,
        runtimeMode,
    }
}
ts 复制代码
export function selectTasklistAgentRuntime(
    invocation: VersionPlanTasklistAgentInvocation | null,
    config: TasklistAgentRuntimeConfig = getTasklistAgentRuntimeConfig()
): TasklistAgentRuntimeSelection | null {
    if (invocation?.kind !== 'ready') {
        return null
    }

    return {
        config,
        runtimeMode: config.runtimeMode,
    }
}

13. 怎么证明迁移没有破坏原有行为

从技术迁移角度看,接入 LangGraph 不是"能跑起来"就够了。

真正要证明的是:

Graph Runner(图编排执行器)和 legacy runner(旧版执行器)在关键路径上行为等价。

这也是我为什么先抽共享 Step Operation,而不是直接另写一套 Graph 逻辑。

验证重点主要包括:

text 复制代码
ready plan 可以生成 v1、校验并 final
needs_review 可以继续并输出人工复核点
ask_clarification 会进入 stopped,不生成 draft
stop_with_boundary_message 会进入 stopped,不生成 draft
optional context 成功后继续生成 strategy 和 tasklist
optional context 失败后降级继续,并输出人工复核点
warning 只有人工复核项时不触发 v2
fail 或 fixNow 时最多生成 v2
v2 必须再次校验
v2 后不生成 v3
final 前必须完成 revision effect
final tasklist 正文只通过 Agent Text Artifact 展示
普通问答和 docs summary 不退化

这些测试的目的不是证明 LangGraph 多强,而是证明:

换了编排表达方式,受控边界没有变。

这点非常重要。

如果接入 Graph 后,Agent 可以多读资源、多修几次、绕过最终输出守卫,或者普通问答主链路被影响,那迁移就失败了。

所以这版的验证重点不是"Graph 更智能",而是:

Graph Runner 能不能在行为边界上对齐旧版 Runner。

ts 复制代码
async function runGraphRunner(...responses: string[]) {
    return runGraphRunnerWithEnv(
        { AI_MIND_TASKLIST_AGENT_RUNTIME: 'graph' },
        ...responses
    )
}

it('ready plan 可生成 v1、校验并 final,且 tasklist 正文只通过 artifact 输出', async () => {
    const { model, result, writtenChunks } = await runGraphRunner(proceedPlanningOutput, validTasklist)

    expect(result.state.status).toBe('final')
    expect(result.graphState.output?.status).toBe('final')
    expect(result.state.artifacts.tasklistDraft?.version).toBe(1)
    expect(result.state.artifacts.tasklistDraft?.validationV1?.status).toBe('pass')
    expect(result.state.artifacts.planning.revisionEffect?.finalDecision).toBe('final')
    expect(model.invoke).toHaveBeenCalledTimes(2)

    // 最终 tasklist 通过 Artifact 输出,不混进普通 text-delta。
    expect(writtenChunks.some(chunk => chunk.type === 'artifact-start')).toBe(true)
    expect(writtenChunks.some(chunk => chunk.type === 'text-delta' && chunk.delta.includes('## Step 1:Graph State'))).toBe(false)
})

完整测试还覆盖了 Graph 事件开关、调试摘要脱敏、追问澄清、边界停止、可选上下文失败降级、warning 分流和 v2 后不再修正等路径。这里保留一个最小测试片段,用来说明迁移后最重要的产物语义没有变化。


14. Graph 事件:我没有透传 LangGraph 原始调试事件流

接入 Graph 后,很自然会想展示 Graph 执行过程。

但我没有把 LangGraph 原始调试事件直接透传给前端。

原因有三个。

第一,AI Mind 已经有自己的 stream-core 协议。

前端消费的是 AI Mind 的流式 chunk,而不是某个框架的内部事件。

第二,LangGraph 原始事件结构会把前端和框架实现强绑定。

以后如果框架事件变了,前端也会被迫跟着变。

第三,原始事件可能携带过多内部状态。

比如 prompt、model messages、draft、tool raw output,这些不应该直接展示给前端。

所以这版新增的是 AI Mind 自己的 Graph 事件:

text 复制代码
agent-graph-node-start
agent-graph-node-end
agent-graph-route
agent-graph-state-patch

它们只表达产品化执行过程需要的信息:

text 复制代码
哪个 node(节点)开始
哪个 node(节点)完成
走了哪条 route(路由)
产生了什么 state patch(状态补丁)摘要
耗时多久
有没有 warning / error

它们不会传:

text 复制代码
完整 prompt
完整 model messages
完整 version plan 原文
完整 optional context 原文
完整 tasklist draft
完整 tool raw output
完整 GraphState

所以这里的核心判断是:

Graph 事件不是 LangGraph 调试事件流的转发,而是 AI Mind 自己的受控可观察协议。

ts 复制代码
export function withGraphNodeEvents(
    nodeId: VersionPlanTasklistGraphNodeId,
    node: GraphNodeHandler,
    runtime: VersionPlanTasklistGraphNodeRuntime
): GraphNodeHandler {
    if (!runtime.runtimeConfig.graphEventsEnabled) {
        return node
    }

    return async function graphNodeWithEvents(state) {
        const eventBase = getGraphEventBase(state)
        const nodePartId = createId()

        emitGraphNodeStart(runtime.writeChunk, {
            ...eventBase,
            nodeId,
            partId: nodePartId,
            title: getGraphNodeTitle(nodeId),
        })

        const update = await node(state)
        const graphUpdate = getGraphRuntimeUpdate(update)

        for (const patch of graphUpdate?.statePatchSummaries ?? []) {
            emitGraphStatePatch(runtime.writeChunk, { ...eventBase, nodeId: patch.nodeId, patchSummary: patch.summary })
        }

        for (const route of graphUpdate?.routes ?? []) {
            emitGraphRoute(runtime.writeChunk, { ...eventBase, route })
        }

        emitGraphNodeEnd(runtime.writeChunk, {
            ...eventBase,
            nodeId,
            partId: nodePartId,
            status: 'completed',
            summary: getGraphUpdateSummary(update, nodeId),
        })

        return update
    }
}

emitGraphNodeStartemitGraphRouteemitGraphStatePatch 等函数只负责输出脱敏后的 node、route 和状态变更摘要。完整 prompt、version plan、tasklist draft 和 GraphState 都不会通过这些事件直接发给前端。


15. AgentTracePanel:从执行步骤到 Graph 时间线

前端展示上,AgentTracePanel(执行过程面板)也跟着升级。

但我没有把它做成复杂拓扑图。

它仍然是 timeline(时间线),只是可以展示更多 Graph 信息:

text 复制代码
运行模式标识:LangGraph
node 数量
nodeId
路由摘要
状态变更摘要
耗时
warning 标签
人工复核点
调试摘要折叠区

它不展示:

text 复制代码
完整 prompt
完整 action JSON
完整 TasklistStrategy JSON
完整 AgentState
完整 GraphState
完整 draft diff
完整 tool output

整体链路可以理解成:

text 复制代码
StateGraph node / route / patch
  -> AI Mind Graph 事件
  -> stream reducer
  -> AgentStepPart.graph
  -> AgentTracePanel 时间线
flowchart LR subgraph Exec[&#34;后端:StateGraph 执行&#34;] N[&#34;node start / end&#34;] R[&#34;route&#34;] P[&#34;state patch summary&#34;] end subgraph Events[&#34;AI Mind Graph 事件&#34;] E1[&#34;agent-graph-node-start / end&#34;] E2[&#34;agent-graph-route&#34;] E3[&#34;agent-graph-state-patch&#34;] end subgraph Frontend[&#34;前端流式聚合&#34;] Reducer[&#34;stream reducer&#34;] Part[&#34;AgentStepPart.graph&#34;] Panel[&#34;AgentTracePanel<br/>Graph 时间线&#34;] end N --> E1 R --> E2 P --> E3 E1 --> Reducer E2 --> Reducer E3 --> Reducer Reducer --> Part Part --> Panel Raw[&#34;LangGraph 原始调试事件流&#34;] -. &#34;不直接透传&#34; .-> Events

这样前端看到的是:

Agent 执行过程摘要。

而不是:

框架内部调试台。

最终 tasklist 仍然由 AgentTextArtifactPanel 展示。

Trace 展示过程,Artifact 展示产物。

这两个职责没有混在一起。

ts 复制代码
case 'agent-graph-node-start':
    return updateActiveMessage(state, (messages, messageId) =>
        upsertAgentGraphNodePart(messages, messageId, {
            nodeId: chunk.nodeId,
            partId: chunk.partId,
            patchSummaries: [],
            status: 'running',
            stepIndex: chunk.stepIndex,
            title: chunk.title,
        }, chunk.runId, chunk.agentName)
    )

case 'agent-graph-route':
    return updateActiveMessage(state, (messages, messageId) =>
        appendAgentGraphRoutePart(messages, messageId, {
            fromNodeId: chunk.fromNodeId,
            routeLabel: chunk.routeLabel,
            toNodeId: chunk.toNodeId,
            reason: chunk.reason,
        }, chunk.runId, chunk.agentName)
    )

case 'agent-graph-state-patch':
    return updateActiveMessage(state, (messages, messageId) =>
        upsertAgentGraphNodePart(messages, messageId, {
            nodeId: chunk.nodeId,
            patchSummaries: [chunk.patchSummary],
        }, chunk.runId, chunk.agentName)
    )

这里保留的是 reducer 层的聚合入口:Graph 事件先进入消息树,再由 AgentTracePanel 统一展示为 Graph 时间线。完整的 message operation 和 JSX 展示代码不展开,否则会把文章重心从 Agent Graph 迁移带到前端组件实现上。


16. Checkpoint 和调试摘要:开发态入口,不是产品能力

接入 LangGraph 后,checkpoint(检查点)、resume(恢复执行)、replay(重放)、HITL(人工介入)这些能力很容易被提起。

但在 v0.2.0,我没有把这些做成用户产品能力。

这版最多只做开发态 memory checkpoint:

text 复制代码
AI_MIND_GRAPH_CHECKPOINT=memory

并且边界非常明确:

text 复制代码
只允许 development
production 强制 off
不承诺刷新后恢复
不承诺服务重启后恢复
不提供历史 run 列表
不提供 resume
不提供 replay
不提供 state edit
不做 HITL 审批按钮

调试摘要(Debug Summary)也是只读的。

它只展示白名单摘要字段,比如:

text 复制代码
runId
threadId
runtimeMode
checkpointMode
currentNode
visitedNodes
lastRoute
readiness.status
decision.type
strategy.granularity
validation score
revisionEffect.finalDecision
limits

它不展示完整 state JSON。

更不会展示完整 prompt、完整 version plan、完整 tasklist draft 或完整 tool output。

所以这里要强调:

Checkpoint 在这一版不是用户态能力,而是开发态观察入口。

它的意义是给后续 checkpoint、interrupt、HITL、replay 留一个更自然的架构位置,而不是现在就承诺这些产品能力。


17. 这版仍然没有放开 Agent 边界

虽然接入了 LangGraph,但这版仍然没有把 Tasklist Agent 扩成通用 Agent:它不开放自由工具调用,不允许模型自由读取资源,不从裸目标生成 tasklist,也不会扫描目录或写入 docs 文件。

同时,checkpoint、调试摘要和 Graph 时间线也都只是受控观察能力,不是生产级恢复执行、重放、人工审批或状态编辑。这样做的目的很简单:先验证"受控 Agent 主路径能否稳定迁移到 Graph 编排",而不是把所有 Agent 能力一次性打开。


18. 这版真正锻炼的是什么

做完 v0.2.0 后,我对 LangGraph 的理解更具体了。

它不是只能用于开放式 Agent,也可以用于受控 Agent。关键在于,我们怎么定义它的位置。

在 AI Mind 里,LangGraph 不是权限层,也不是安全边界的事实源。

它是编排表达层。

真正的边界仍然来自:

text 复制代码
入口判断
资源白名单
工具权限范围
状态机约束
步骤上限
修正次数上限
最终输出守卫
流式协议
UI 展示边界

所以这版真正锻炼的不是 LangGraph API,而是三件事:

  1. 怎么在不扩大 Agent 权限的前提下迁移编排层;
  2. 怎么保证旧版 Runner 和 Graph Runner 行为等价;
  3. 怎么把框架内部事件转成产品可消费的执行过程,而不是把调试事件流原样扔给前端。

如果用一句话总结 v0.2.0

这版不是让 Tasklist Agent 更开放,而是把一条已经受控的 Agent 主路径,迁移成更显式、更可观察,也更适合后续演进的 Graph 编排。

这也是我觉得"从手写 Runner 到 LangGraph"真正有价值的地方。


项目地址

👉 GitHub:github.com/HWYD/ai-min...

如果这篇文章或者 AI Mind 项目对你有所帮助,也欢迎顺手帮项目点个 Star⭐。这个支持对我来说很重要,也会让我更有动力继续整理后续版本的实现过程、设计取舍和踩坑复盘。

相关推荐
UXbot1 小时前
AI网页开发工具能替代工具吗?5大平台对比
前端·人工智能·低代码·ui·原型模式·web app
wuhen_n1 小时前
从零到一!前端搭建本地轻量化 RAG 问答系统
前端·langchain·ai编程
谁在黄金彼岸1 小时前
Lance模型解读
后端
落日漫游1 小时前
代码报错难排查?借助Gemini快速修复
前端
niconicoC1 小时前
让 Three.js 场景更真实:我用高斯泼溅和 SparkJS 做了一个可交互的 3D Demo
前端·webgl
神奇小汤圆1 小时前
深入理解MySQL事务隔离级别:MVCC机制与Next-Key Lock如何解决幻读问题?
后端
Darling噜啦啦1 小时前
JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心
前端·javascript·数据结构
万少1 小时前
一封邮件,让我重新打开了搁置半年的鸿蒙应用
前端·javascript·后端
wjj不想说话1 小时前
你的小程序活动页,可能已经成了后台配置的杂物间
前端