当前位置: 首页 > news >正文

[Parttern] Rebuilding LangGraph’s “Messages Magic” from First Principles

Zod v4 + Schema Metadata + Reducer-Based State Compilation

This is a single, self-contained learning note.
It explains why LangGraph works, how the internals are structured, and includes a minimal working implementation split into library code and user code.


TL;DR

LangGraph is not magical. It does three things:

  1. Attach semantics (reducers, defaults) to Zod schemas via metadata
  2. Compile schemas into per-field runtime “channels”
  3. Execute nodes by applying reducers to merge updates

Everything else is ergonomics.


Mental Model (keep this)

Layer Responsibility
Zod schema State shape
Schema meta Merge semantics
Registry Bind shape → semantics
Compiler Shape + semantics → execution plan
Runtime Execute nodes + apply reducers

Why metadata exists

Zod answers validation questions.
It does not answer state-merge questions.

Examples Zod cannot answer:

  • “Append or overwrite?”
  • “Replace by id or always push?”
  • “What’s the default value?”

So LangGraph keeps Zod pure and adds out-of-band metadata.


Why metadata is attached to schema instances

This must be legal:

const ChatSchema = z.object({messages: chatMessagesSchema, // merge-by-id
})const AuditSchema = z.object({messages: auditMessagesSchema, // append-only
})

Same key name, different semantics.
Hence:

WeakMap<ZodSchemaInstance, SchemaMeta>
```### Design patterns involvedCompiler pattern: schema → runtime planStrategy pattern: per-field reducerDecorator / Annotation: schema metadataTypeclass-like dispatch: “for this type, here’s how it merges”### Minimal ImplementationEverything below is runnable TypeScript.lib/state-graph.ts (library code)```ts
import { z } from 'zod/v4'
import { randomUUID } from 'node:crypto'/* ---------- core types ---------- */export type Reducer<TValue, TUpdate> = (left: TValue, right: TUpdate) => TValueexport type SchemaMeta<TValue, TUpdate = TValue> = {default?: () => TValuereducer?: {fn: Reducer<TValue, TUpdate>}
}export type Channel<TValue, TUpdate> = {default: () => TValuereducer: Reducer<TValue, TUpdate>
}export type Node<TState> = (state: TState) => Promise<Partial<TState>> | Partial<TState>/* ---------- metadata registry ---------- */
/*** Matches Zod v4 `.register(registry, meta)` expectations:* registry must implement `.add(schema, meta)`*/
export class MetaRegistry {private map = new WeakMap<object, SchemaMeta<any, any>>()add<TValue, TUpdate>(schema: object, meta: SchemaMeta<TValue, TUpdate>) {this.map.set(schema, meta)return schema}get(schema: object) {return this.map.get(schema)}
}/* ---------- compiler ---------- */
/*** Compile Zod object schema into runtime channels.* Each channel knows:* - how to initialize default state* - how to merge updates (reducer strategy)*/
export function compileChannels(schema: z.ZodObject<any>, registry: MetaRegistry) {const shape = schema.shapeconst channels: Record<string, Channel<any, any>> = {}for (const [key, fieldSchema] of Object.entries(shape)) {const meta = registry.get(fieldSchema as any)if (meta?.reducer) {channels[key] = {default: meta.default ?? (() => undefined),reducer: meta.reducer.fn,}} else {// last-write-wins fallbackchannels[key] = {default: meta?.default ?? (() => undefined),reducer: (_left: any, right: any) => right,}}}return channels
}/* ---------- runtime ---------- */
/*** Execute nodes sequentially.* State updates are merged via compiled reducers.*/
export async function runGraph<TState extends Record<string, any>>(schema: z.ZodObject<any>,registry: MetaRegistry,nodes: Array<Node<TState>>,initial: TState
) {const channels = compileChannels(schema, registry)let state: TState = { ...initial }// initialize defaultsfor (const [key, ch] of Object.entries(channels)) {if (state[key as keyof TState] === undefined) {state[key as keyof TState] = ch.default()}}// execute nodesfor (const node of nodes) {const update = await node(state)for (const [key, value] of Object.entries(update)) {const ch = channels[key]if (!ch) continuestate[key as keyof TState] = ch.reducer(state[key], value)}}return state
}/* ---------- built-in reducer preset ---------- */export type Msg = {id?: string | nullcontent: stringrole: 'human' | 'ai'
}/*** Merge-by-id messages reducer:* - ensure every message has an id* - replace existing message with same id* - append otherwise*/
export function messagesReducer(left: Msg[], right: Msg[]) {const ensureId = (m: Msg) => {if (m.id == null) m.id = randomUUID()return m}const merged = left.map(ensureId)const byId = new Map(merged.map((m, i) => [m.id as string, i]))for (const raw of right) {const m = ensureId(raw)const idx = byId.get(m.id as string)if (idx != null) {merged[idx] = m} else {byId.set(m.id as string, merged.length)merged.push(m)}}return merged
}
```demo.ts (user / application code)```ts
import { z } from 'zod/v4'
import {MetaRegistry,runGraph,messagesReducer,type Msg,type SchemaMeta,
} from './lib/state-graph'/* ---------- registry + policy ---------- */const registry = new MetaRegistry()const MessagesMeta: SchemaMeta<Msg[], Msg[]> = {default: () => [],reducer: { fn: messagesReducer },
}/* ---------- schema ---------- */
/*** Meta is attached via Zod’s native `.register()`.* Zod will call `registry.add(schema, meta)`.*/
const Schema = z.object({messages: z.array(z.object({id: z.string().optional().nullable(),content: z.string(),role: z.enum(['human', 'ai']),})).register(registry, MessagesMeta),
})type State = z.infer<typeof Schema>/* ---------- nodes ---------- */const node1 = () => ({messages: [{ role: 'human', content: 'Hello, how are you?' }],
})const node2 = () => ({messages: [{ role: 'ai', content: "I'm good, thank you!" }],
})const node3 = () => ({messages: [{ role: 'human', id: 'm-123', content: 'Corrected message' }],
})const node4 = () => ({messages: [{ role: 'ai', id: 'm-456', content: 'Corrected AI message' }],
})/* ---------- run ---------- */const result = await runGraph<State>(Schema, registry, [node1, node2, node3, node4], {messages: [{ role: 'human', id: 'm-123', content: 'Initial human message' },{ role: 'ai', id: 'm-456', content: 'Initial AI message' },],
})console.log(result)
```###Why this matches LangGraph exactlyUses Zod v4 native .register()Registry implements .add() like LangGraph’sReducers are field-level strategiesRuntime is reducer-agnosticMessages behavior is opt-in policy, not hard-coded### The Big InsightLangGraph is a deterministic state-machine compiler.
LLM calls are just nodes.
Reducers are the real power.Once you see that, nothing feels magical again.### Where to go nextParallel branches → associative reducersStreaming → partial updates as reducer inputsCRDT-style reducersVisualizing compiled channelsAt that point, you’re not using LangGraph.You’re designing execution semantics.
http://www.jsqmd.com/news/350198/

相关文章:

  • 2026中国细胞储存行业竞争力蓝皮书:免疫细胞引领健康储备新潮流 - 速递信息
  • 怎样解决多样场景下的甲醛污染,选科立恩除甲醛
  • 2026年除甲醛靠谱公司推荐指南:科立恩等品牌解析
  • 2026年知名的智能检测门,校园安检门,手机检测门厂家选型推荐名录 - 品牌鉴赏师
  • 天津山誉科技发展有限公司产品更新频率怎样?打印机租赁费用高吗 - 工业推荐榜
  • 阅读笔记一
  • 2026年云南冷藏车服务厂商哪家好,专业厂家排名公布 - 工业品网
  • 2026年亳州职业教育排名,安徽新华电脑专修学院电话及特色解读 - 工业推荐榜
  • 2026年医疗电动推杆工厂推荐榜:精密驱动与静音平稳的医疗设备核心动力源头厂家深度解析 - 品牌企业推荐师(官方)
  • 如何选除甲醛品牌?十大品牌来支招
  • 2026年全国十大除甲醛品牌推荐指南:科立恩等优选分析
  • 当 AI 开始修复CSRF漏洞,我知道它不只是工具
  • 彩色鹅卵石采购价格多少,靠谱厂家有哪些 - 工业品牌热点
  • 小程序开发路径与关键决策解析,助力业务高效拓展线上渠道
  • 写作小白救星!抢手爆款的降AI率软件 —— 千笔·专业降AIGC智能体
  • 计算机毕业设计springboot蔬菜销售系统 基于SpringBoot的生鲜电商平台设计与实现 SpringBoot+MySQL的农产品在线交易系统研发
  • 2026年河北液压接头专业厂家口碑排名,前十名有哪些? - 工业设备
  • 【期货量化进阶】期货量化交易策略因子挖掘方法(Python量化)
  • 长治市英语雅思培训辅导机构推荐-2026权威出国雅思课程中心学校口碑排行榜 - 苏木2025
  • 2026年全国热解炉哪家强?多家靠谱厂家全维度剖析 差异化服务一览 - 深度智识库
  • 2026年可靠的监狱手机检测门,高精度手机探测门,智能手机探测门厂家行业口碑榜单 - 品牌鉴赏师
  • 使用stm32CubeProgrammer连续升级程序
  • 2026年 MBR膜源头厂家实力排行榜:专业水处理膜技术,高效分离与长效稳定运行口碑之选 - 品牌企业推荐师(官方)
  • 西门子S7-1500暖通空调制药厂洁净空调PLC程序案例,硬件采用西门子1500CPU+ET2...
  • 2026年共聚焦显微镜大搜罗:实力厂、售后优、本地优产齐亮相 - 品牌推荐大师
  • 2026 最新 AI 论文智能写作软件横向测评,核心功能对比榜单出炉
  • 在root下升级Node.js到22+
  • 避免 OOM,高效导出百万级数据的 SpringBoot 实现方案
  • 长治市英语雅思培训辅导机构推荐,2026权威出国雅思课程中心学校口碑排行榜 - 苏木2025
  • 奇了怪,Select for update 到底加了个什么锁?