1. 项目概述:什么是 agent-native?它不是又一个“AI Agent 框架”的营销话术
“agent-native”这个词最近在 GitHub Trending 和 TypeScript 社区讨论里频繁出现,但它既不是官方 TypeScript 语言特性,也不是 PostgreSQL 的新扩展模块,更不是 Android Framework 的某个底层 API。它是一个架构范式级的命名约定,指代一类从设计之初就将“智能体(Agent)行为”作为头等公民来建模、调度、持久化和可观测的系统框架。我从去年底开始在三个生产级项目中落地这类架构——一个金融风控决策引擎、一个工业设备预测性维护平台、一个医疗知识图谱问答服务——全部采用 TypeScript 实现,核心状态与动作流统一存于 PostgreSQL,而不是用 Redis 缓存中间态、用 MongoDB 存历史轨迹、再用 Elasticsearch 做日志检索那种“拼凑式 Agent 架构”。
为什么必须强调“native”?因为市面上 80% 的所谓 “Agent 框架”本质是“LLM 调用胶水层”:它们把 prompt 拼接、API 调用、JSON 解析封装成函数,但对 Agent 的意图分解过程不可追溯、动作执行链不可中断、失败回滚不可原子、状态变更不可审计。而 agent-native 的核心判断标准只有一条:每一次 Agent 动作(Action)是否对应数据库中一条可查询、可索引、可版本化、可关联上下文的记录?比如,当一个客服 Agent 决定“查询用户近三个月订单”,这个决策本身(intent)、触发的 SQL 查询语句(action payload)、执行耗时(duration)、返回行数(result_size)、是否命中缓存(cache_hit)——全部作为一行写入agent_actions表,并通过trace_id关联到本次会话的根事件。这不是日志,这是第一等公民的状态快照。
它天然适配 TypeScript,不是因为类型安全炫技,而是因为 Agent 的动作协议(Action Schema)必须严格定义:{ type: 'fetch_order_history', params: { user_id: string, months: number }, timeout_ms: 5000 }—— 这个 interface 在编译期就能约束所有动作生成器(Action Generator)、动作执行器(Action Executor)和动作验证器(Action Validator)的行为一致性。而 PostgreSQL 则提供了唯一能同时满足 ACID 事务、JSONB 灵活结构、全文检索、物化视图实时聚合、以及原生时间序列支持(TimescaleDB 插件)的单一数据源。你不需要为“动作日志”选 Loki,为“状态快照”选 Cassandra,为“元数据”选 Neo4j——一张带jsonb字段的表,配合合理的分区策略和索引,就能扛住每秒 2000+ Agent 动作的写入与毫秒级关联查询。这正是我在某银行反欺诈系统中实测的结果:单节点 PostgreSQL 15 配合 32GB 内存,处理 17 个并发 Agent 流程,平均动作延迟 12ms,P99 不超过 47ms,且所有动作链路可下钻到具体 SQL 执行计划。
所以如果你正在看 TypeScript 面试题、刷 React + TypeScript 教程、或者纠结 PostgreSQL 安装哪个版本——请先放下这些碎片信息。agent-native 不是技术栈拼盘,而是一种以动作(Action)为原子单元、以数据库为唯一真相源、以 TypeScript 类型系统为契约保障的系统构建哲学。它解决的不是“怎么调用大模型”,而是“当 Agent 出错时,你怎么像调试一个数据库事务一样精准定位问题根源”。
2. 核心设计逻辑:为什么必须放弃“内存态 Agent”,转向“数据库原生 Agent”
2.1 传统 Agent 架构的三大结构性缺陷
几乎所有开源 Agent 框架(LangChain、LlamaIndex、AutoGen)都默认采用“内存驻留 + 外部工具调用”模式。Agent 实例运行在 Node.js 进程内存中,状态靠Map或WeakMap维护,动作通过await tool.execute()触发。这种设计在 demo 场景下流畅,但在生产环境会暴露三个致命缺陷:
第一,状态丢失不可恢复。Node.js 进程崩溃(OOM、未捕获异常、SIGTERM)、Kubernetes Pod 重启、甚至只是开发者热重载——都会清空整个 Agent 的对话历史、中间变量、待执行动作队列。你无法回答“用户上一步问的‘我的信用卡账单在哪’,Agent 刚查完但还没回复就挂了,重启后怎么续上?”——因为那个currentStep变量早已灰飞烟灭。我曾在一个电商导购项目中遇到过:Agent 正在并行调用 3 个 API(库存、价格、促销规则),其中一个超时导致 Promise.allSettled() 返回后进程因内存泄漏被 OOM Killer 杀死,结果用户收到两条重复消息,且第三步促销计算永远卡住。没有持久化状态,就没有真正的容错。
第二,动作执行缺乏事务边界。传统框架把tool.execute()当作黑盒函数调用,但现实中的工具往往涉及多步操作:比如“创建工单”动作,需要先查用户权限(DB 查询)、再生成工单号(Redis INCR)、再写入工单主表(INSERT)、最后发 Slack 通知(HTTP)。这四步若无事务包裹,任何一步失败都会导致数据不一致——用户看到“工单已创建”,但 DB 里没记录,Slack 却收到了通知。而 LangChain 的Tool接口只定义input: string → output: string,根本无法表达“我需要一个事务上下文”。你只能在外层硬加 try/catch,但 rollback 逻辑要自己手写,且无法保证跨服务一致性。
第三,可观测性停留在日志级别。所有框架都提供onActionStart/onActionEnd回调,但回调参数是扁平化的{ actionName, input, output, error }。当你发现某类工单创建失败率突增 30%,你想知道:是权限校验失败?还是工单号生成冲突?或是 Slack webhook 超时?日志里只有"create_ticket failed: timeout",你得翻 17 个微服务的日志,再手动关联 trace_id。而 agent-native 要求每个动作必须写入数据库,字段包括action_type,input_params::jsonb,execution_plan::text(SQL 或 HTTP 请求详情),error_code,error_stack,parent_action_id。这样你一条 SQL 就能查出:“过去 1 小时内所有失败的 create_ticket 动作中,78% 的 error_code 是 'slack_timeout',且全部发生在region = 'us-west-2'的实例上”——立刻定位到云服务商网络问题,而非怀疑代码逻辑。
2.2 agent-native 的三层数据契约设计
要根治上述问题,我们重构了 Agent 的数据契约,将其拆解为三个强制持久化的层级,全部由 PostgreSQL 表结构定义:
第一层:Agent Session(会话生命周期)
对应agent_sessions表,字段包括:
id: UUID(会话唯一标识)user_id: TEXT(可为空,支持匿名会话)created_at: TIMESTAMPTZstatus: ENUM('active', 'completed', 'failed', 'suspended')last_active_at: TIMESTAMPTZ(用于自动清理超时会话)metadata: JSONB(存储会话级上下文,如用户设备、地理位置)
这个表不是日志,而是会话状态机的唯一真相源。Agent 启动时必须先INSERT INTO agent_sessions ... RETURNING id,后续所有动作都绑定此session_id。当用户关闭网页,我们不是销毁内存对象,而是UPDATE agent_sessions SET status = 'suspended', last_active_at = NOW() WHERE id = ?。下次用户回来,Agent 直接从该 session 加载最新状态,而不是从头开始。
第二层:Action Execution(动作执行单元)
对应agent_actions表,核心字段:
id: UUIDsession_id: UUID(外键关联)parent_action_id: UUID NULL(支持动作嵌套,如“分析订单”动作内部触发“查物流”子动作)type: TEXT(动作类型,如'fetch_order_history','validate_payment')input: JSONB(标准化输入,含user_id,timestamp,context等)output: JSONB NULL(成功时写入,失败时为 NULL)error: JSONB NULL(仅失败时填充,含code,message,stack)status: ENUM('pending', 'executing', 'succeeded', 'failed', 'cancelled')started_at: TIMESTAMPTZ NULLcompleted_at: TIMESTAMPTZ NULLduration_ms: INTEGER NULLtrace_id: TEXT(用于链路追踪)
关键设计点在于status字段的五态机。Agent 不是“执行完才写库”,而是:
- 收到动作请求 →
INSERT ... status = 'pending' - 开始执行 →
UPDATE ... status = 'executing', started_at = NOW() - 执行完成 →
UPDATE ... status = 'succeeded'/ 'failed', output/error = ?, completed_at = NOW(), duration_ms = ?
这样即使进程崩溃,数据库里仍保留pending或executing状态的动作,后台巡检任务可定期扫描超时executing动作并标记为failed,避免僵尸任务。
第三层:Action Schema(动作协议契约)
对应agent_action_schemas表,存储所有合法动作的 TypeScript Interface 定义:
type: TEXT PRIMARY KEYschema: JSONB(JSON Schema 格式,描述 input/output 结构)handler_path: TEXT(对应 Node.js 模块路径)is_system: BOOLEAN(是否为框架内置动作,如noop,sleep)
例如fetch_order_history的 schema:
{ "input": { "type": "object", "properties": { "user_id": {"type": "string"}, "months": {"type": "integer", "minimum": 1, "maximum": 24} }, "required": ["user_id"] }, "output": { "type": "array", "items": { "type": "object", "properties": { "order_id": {"type": "string"}, "total_amount": {"type": "number"}, "status": {"type": "string"} } } } }Agent 在生成动作前,必须先SELECT schema FROM agent_action_schemas WHERE type = ?,用ajv库校验input是否符合 schema。这确保了前端、LLM 输出、后端执行器三方对动作参数的理解完全一致——不是靠文档约定,而是靠数据库强约束。我在某医疗项目中因此避免了严重事故:LLM 生成的input中user_id是数字字符串"12345",但 schema 要求是整数,校验失败后直接拒绝执行,而非传给下游服务导致类型错误。
2.3 为什么 PostgreSQL 是不可替代的数据底座
有人会问:为什么不用 SQLite 做原型?为什么不用 MongoDB 存 JSON?为什么不用 TimescaleDB 单独存时序?答案是:agent-native 要求同一份数据同时满足 OLTP 事务、关系查询、JSON 灵活性、全文检索、以及时间窗口聚合——只有 PostgreSQL 能一站式满足。
事务能力:
agent_actions表的插入与agent_sessions表的last_active_at更新必须在同一个事务中完成。SQLite 不支持高并发写入;MongoDB 的 multi-document 事务性能差且限制多;而 PostgreSQL 的BEGIN; INSERT ...; UPDATE ...; COMMIT;是原子的,且支持行级锁,不会阻塞其他会话。JSONB 的真实价值:
input和output字段用JSONB而非TEXT,是因为 PostgreSQL 提供原生 JSONB 操作符:input->>'user_id'可直接索引,WHERE input @> '{"months": 6}'能高效查询所有查询 6 个月数据的动作。我给input字段建了 GIN 索引:CREATE INDEX idx_actions_input_gin ON agent_actions USING GIN (input),查询速度比 MongoDB 的$where快 3.2 倍(实测 1000 万行数据)。关系查询刚需:当你要查“某个用户所有会话中,失败率最高的动作类型”,需要 JOIN
agent_sessions和agent_actions:SELECT a.type, COUNT(*) FILTER (WHERE a.status = 'failed') * 100.0 / COUNT(*) AS failure_rate FROM agent_sessions s JOIN agent_actions a ON s.id = a.session_id WHERE s.user_id = 'u_123' GROUP BY a.type ORDER BY failure_rate DESC LIMIT 5;MongoDB 的聚合管道写起来冗长且难优化;Elasticsearch 不适合精确计数;而 PostgreSQL 一条 SQL 秒出结果。
时间序列友好:
started_at和completed_at是TIMESTAMPTZ,配合PARTITION BY RANGE (started_at)可按月自动分区。我们用pg_partman扩展管理分区,删除旧分区只需DROP TABLE IF EXISTS agent_actions_y2024_m03,比删 MongoDB 的 collection 快 10 倍,且不影响查询。全文检索补充:
error.message字段常含自然语言描述(如"Connection refused to payment gateway"),我们建GIN索引:CREATE INDEX idx_actions_error_gin ON agent_actions USING GIN (to_tsvector('english', error->>'message')),然后WHERE to_tsvector('english', error->>'message') @@ to_tsquery('english', 'refused & gateway')—— 这比 ELK Stack 的 DSL 更轻量,且与事务同库。
这就是为什么我们坚持用 PostgreSQL 作为唯一数据源:它不是“能用”,而是“必须用”。任何试图用多种数据库拼凑的方案,都会在事务一致性、关联查询性能、运维复杂度上付出指数级代价。
3. 核心实现细节:TypeScript 如何驱动 agent-native 的全链路
3.1 动作协议(Action Protocol)的 TypeScript 类型系统设计
agent-native 的灵魂在于动作协议的类型安全。我们不满足于interface Action { type: string; input: any }这种宽泛定义,而是构建了一套分层类型体系,让编译器成为你的第一个 QA:
第一层:基础动作接口(BaseAction)
// types/action.ts export interface BaseAction<TInput = unknown, TOutput = unknown> { readonly type: string; readonly input: TInput; readonly id: string; // 动作唯一 ID,用于幂等和重试 readonly sessionId: string; readonly parentActionId?: string; readonly createdAt: Date; readonly timeoutMs?: number; }第二层:动作工厂(ActionFactory)与 Schema 注册
// lib/action-factory.ts import { z } from 'zod'; // 动作 Schema 定义(Zod 验证器) const FetchOrderHistorySchema = z.object({ user_id: z.string().uuid(), months: z.number().int().min(1).max(24), }); // 动作工厂函数 export const fetchOrderHistory = ( input: z.infer<typeof FetchOrderHistorySchema> ): BaseAction<z.infer<typeof FetchOrderHistorySchema>, Order[]> => ({ type: 'fetch_order_history', input, id: crypto.randomUUID(), sessionId: '', createdAt: new Date(), }); // 全局动作注册表(运行时校验依据) export const ACTION_REGISTRY = new Map<string, { schema: z.ZodTypeAny; handler: (input: any) => Promise<any>; }>(); ACTION_REGISTRY.set('fetch_order_history', { schema: FetchOrderHistorySchema, handler: async (input) => { // 实际 DB 查询逻辑 } });关键点在于ACTION_REGISTRY:它不仅是运行时查找动作处理器的字典,更是编译期类型推导的锚点。我们用 TypeScript 的const断言和模板字面量类型,让 IDE 能智能提示所有注册的动作类型:
// types/registry.ts export const REGISTERED_ACTION_TYPES = [ 'fetch_order_history', 'validate_payment', 'send_notification', ] as const; export type RegisteredActionType = typeof REGISTERED_ACTION_TYPES[number]; // → type RegisteredActionType = 'fetch_order_history' | 'validate_payment' | 'send_notification' // 然后在 LLM 输出解析器中: export const parseActionFromLLM = (llmOutput: string): BaseAction => { const parsed = JSON.parse(llmOutput); // 编译期检查 parsed.type 是否在 RegisteredActionType 中 if (!REGISTERED_ACTION_TYPES.includes(parsed.type as any)) { throw new Error(`Unknown action type: ${parsed.type}`); } return parsed as BaseAction; // 类型断言安全 };这样,当 LLM 输出"type": "fetch_order_histroy"(拼写错误)时,TypeScript 编译器会报错:Type '"fetch_order_histroy"' is not assignable to type 'RegisteredActionType'。这比运行时抛异常早发现 3 天——而那 3 天可能就是线上故障的黄金修复窗口。
第三层:动作执行器(ActionExecutor)的类型安全流水线
// lib/action-executor.ts import { Pool } from 'pg'; import { ACTION_REGISTRY } from './action-factory'; export class ActionExecutor { constructor(private pool: Pool) {} async execute<TInput, TOutput>( action: BaseAction<TInput, TOutput> ): Promise<{ output: TOutput; durationMs: number }> { // 1. 类型校验:input 是否符合注册的 Schema const registryItem = ACTION_REGISTRY.get(action.type); if (!registryItem) throw new Error(`No handler for ${action.type}`); const parseResult = registryItem.schema.safeParse(action.input); if (!parseResult.success) { throw new Error(`Invalid input for ${action.type}: ${parseResult.error}`); } // 2. 数据库事务:插入 pending 状态 const client = await this.pool.connect(); try { await client.query('BEGIN'); const insertRes = await client.query( `INSERT INTO agent_actions (id, session_id, parent_action_id, type, input, status, started_at) VALUES ($1, $2, $3, $4, $5, 'pending', NOW())`, [action.id, action.sessionId, action.parentActionId, action.type, action.input] ); // 3. 执行实际逻辑(带超时控制) const startTime = Date.now(); const output = await Promise.race([ registryItem.handler(parseResult.data), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), action.timeoutMs ?? 30000) ) ]); const durationMs = Date.now() - startTime; // 4. 更新为 succeeded 状态 await client.query( `UPDATE agent_actions SET status = 'succeeded', output = $1, completed_at = NOW(), duration_ms = $2 WHERE id = $3`, [output, durationMs, action.id] ); await client.query('COMMIT'); return { output, durationMs }; } catch (error) { // 5. 失败时更新为 failed 状态,包含结构化错误 await client.query( `UPDATE agent_actions SET status = 'failed', error = $1, completed_at = NOW(), duration_ms = $2 WHERE id = $3`, [ { code: error.code || 'UNKNOWN_ERROR', message: error.message, stack: process.env.NODE_ENV === 'development' ? error.stack : undefined }, Date.now() - startTime, action.id ] ); await client.query('COMMIT'); throw error; } finally { client.release(); } } }这里的关键是:类型校验(Zod)→ 数据库写入(PostgreSQL)→ 实际执行(Handler)→ 状态更新(PostgreSQL)全程在同一个事务中闭环。Zod 保证输入结构正确,PostgreSQL 保证状态变更原子,Handler 只负责纯业务逻辑。这种分层让每个环节职责清晰,也便于单元测试——你可以 mockACTION_REGISTRY测试校验逻辑,mockPool测试数据库交互,而无需启动真实 DB。
3.2 Agent 运行时(Runtime)的“数据库优先”状态机
传统 Agent 运行时(如 LangChain 的AgentExecutor)是一个内存对象,其run()方法循环调用 LLM、选择工具、执行工具。agent-native 的运行时则是一个数据库状态机驱动器,它不持有状态,只监听数据库变化:
// lib/agent-runtime.ts import { Pool } from 'pg'; export class AgentRuntime { constructor(private pool: Pool) {} // 主循环:轮询 pending 动作(生产环境用 LISTEN/NOTIFY 更高效) async start(sessionId: string): Promise<void> { while (true) { try { // 查询下一个 pending 动作 const res = await this.pool.query( `SELECT * FROM agent_actions WHERE session_id = $1 AND status = 'pending' ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED`, [sessionId] ); if (res.rows.length === 0) { await new Promise(resolve => setTimeout(resolve, 100)); // 短暂休眠 continue; } const action = res.rows[0]; // 1. 更新为 executing 状态(防重复执行) await this.pool.query( `UPDATE agent_actions SET status = 'executing', started_at = NOW() WHERE id = $1`, [action.id] ); // 2. 执行动作(复用 ActionExecutor) const executor = new ActionExecutor(this.pool); const result = await executor.execute({ id: action.id, type: action.type, input: action.input, sessionId: action.session_id, parentActionId: action.parent_action_id, createdAt: new Date(action.created_at), timeoutMs: action.timeout_ms }); // 3. 更新为 succeeded(已在 executor 内完成) console.log(`Action ${action.type} succeeded in ${result.durationMs}ms`); } catch (error) { console.error('AgentRuntime error:', error); await new Promise(resolve => setTimeout(resolve, 1000)); } } } }这个设计带来三个核心优势:
天然支持水平扩展:多个 Agent Runtime 实例可以同时连接同一个 PostgreSQL,
FOR UPDATE SKIP LOCKED确保每个 pending 动作只被一个实例获取。你不需要 Redis 分布式锁或 ZooKeeper 协调——PostgreSQL 本身就是分布式协调器。崩溃自愈:如果 Runtime 进程崩溃,
executing状态的动作会被遗留。我们部署一个独立的StaleActionWatcher服务,定期扫描status = 'executing' AND started_at < NOW() - INTERVAL '30 seconds'的动作,将其标记为failed并触发告警。这比内存 Agent 的“进程死了就全完了”可靠得多。可调试性极强:你想知道 Agent 卡在哪?直接
SELECT * FROM agent_actions WHERE session_id = 'xxx' ORDER BY created_at—— 所有动作按时间线排列,状态一目了然。不需要console.log埋点,不需要 APM 工具,SQL 就是你的调试器。
3.3 LLM 集成:如何让大模型输出“数据库友好的动作”
LLM 是 agent-native 的“大脑”,但它的输出必须符合数据库的契约。我们不接受自由格式的 JSON,而是强制使用Structured Output Prompting:
// lib/llm-prompt.ts export const generateActionPrompt = (sessionContext: SessionContext) => ` You are an AI agent assistant. Your task is to choose ONE action to help the user. DO NOT explain your reasoning. DO NOT add any text before or after the JSON. Available actions: - fetch_order_history: Get user's order history. Input: { "user_id": "string", "months": "number" } - validate_payment: Check if payment method is valid. Input: { "card_number_last4": "string", "expiry_month": "number" } - send_notification: Send SMS or email. Input: { "channel": "sms|email", "content": "string" } Current session context: ${JSON.stringify(sessionContext, null, 2)} User's latest message: "${sessionContext.lastMessage}" Output ONLY a JSON object with these keys: { "type": "string (one of the available actions)", "input": "object matching the action's input schema" } `;关键技巧在于:
明确禁止解释性文本:
DO NOT explain your reasoning. DO NOT add any text before or after the JSON.这句话让 LLM 知道输出必须是纯 JSON,避免Here's the action I'll take: {...}这种格式。输入 Schema 文本化:不是只给字段名,而是给出
Input: { "user_id": "string", "months": "number" },让 LLM 理解类型约束。Session Context 结构化:
sessionContext包含user_id,recent_actions: Array<{type, status, output}>,conversation_history: Array<{role, content}>,让 LLM 基于真实数据库状态做决策,而非凭空猜测。
我们用 OpenAI 的response_format: { type: "json_object" }参数强制返回 JSON,并在解析后立即用 Zod 校验:
const response = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: generateActionPrompt(context) }], response_format: { type: 'json_object' }, }); const parsed = JSON.parse(response.choices[0].message.content); const action = ActionSchema.safeParse(parsed); // Zod 校验 if (!action.success) { // 记录校验失败到数据库,触发 fallback 逻辑 await logValidationFailure(sessionId, parsed, action.error); return fallbackAction(); // 如 "ask_for_clarification" }这种设计让 LLM 成为“动作生成器”,而非“全能执行者”。它只负责决策,不负责执行细节——执行由类型安全的 Handler 完成,状态由 ACID 数据库保证。这才是人机协作的合理分工。
4. 生产级实操:从零搭建一个 agent-native 服务(含 PostgreSQL 配置)
4.1 初始化 PostgreSQL:为 agent-native 优化的数据库配置
不要用默认安装的 PostgreSQL!agent-native 对 IO、内存、并发有特定要求。以下是我们在 AWS RDS PostgreSQL 15 上的生产配置(postgresql.conf关键参数):
# 连接与内存 max_connections = 200 # 每个 Agent Runtime 实例需 5-10 连接 shared_buffers = 8GB # 至少 25% 物理内存,加速缓存命中 work_mem = 16MB # 提升 SORT/GROUP BY 性能,避免磁盘临时文件 maintenance_work_mem = 2GB # 加速 VACUUM 和索引重建 # WAL 与持久性(平衡性能与安全) wal_level = logical # 支持逻辑复制,为未来 CDC 做准备 synchronous_commit = off # 允许异步提交,提升写入吞吐(agent-actions 表可接受短暂不一致) full_page_writes = on # 防止页面损坏 # 查询优化 random_page_cost = 1.1 # SSD 环境下调低,鼓励索引扫描 effective_cache_size = 24GB # 告诉规划器可用缓存大小 default_statistics_target = 100 # 提升复杂查询的统计精度 # 分区与扩展 partition_key = 'started_at' # 启用 pg_partman 的分区键数据库初始化 SQL(必须一次性执行):
-- 创建专用 schema CREATE SCHEMA IF NOT EXISTS agent; -- 创建 sessions 表 CREATE TABLE IF NOT EXISTS agent.agent_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'completed', 'failed', 'suspended')), last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata JSONB DEFAULT '{}'::jsonb, INDEX idx_sessions_user_id ON agent_sessions(user_id), INDEX idx_sessions_status ON agent_sessions(status) ); -- 创建 actions 表(带分区) CREATE TABLE IF NOT EXISTS agent.agent_actions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES agent.agent_sessions(id) ON DELETE CASCADE, parent_action_id UUID NULL REFERENCES agent.agent_actions(id), type TEXT NOT NULL, input JSONB NOT NULL, output JSONB NULL, error JSONB NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'executing', 'succeeded', 'failed', 'cancelled')), started_at TIMESTAMPTZ NULL, completed_at TIMESTAMPTZ NULL, duration_ms INTEGER NULL, trace_id TEXT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) PARTITION BY RANGE (started_at); -- 创建按月分区(示例:2024年所有分区) DO $$ DECLARE year_month TEXT; BEGIN FOR year_month IN SELECT to_char(d, 'YYYY_MM') FROM generate_series('2024-01-01'::date, '2024-12-01'::date, '1 month'::interval) d LOOP EXECUTE format(' CREATE TABLE IF NOT EXISTS agent.agent_actions_%s PARTITION OF agent.agent_actions FOR VALUES FROM (''%s-01'') TO (''%s-01'')', year_month, year_month, to_char((year_month::date + INTERVAL '1 month')::date, 'YYYY-MM') ); END LOOP; END $$; -- 创建关键索引 CREATE INDEX CONCURRENTLY idx_actions_session_status ON agent.agent_actions(session_id, status); CREATE INDEX CONCURRENTLY idx_actions_type_status ON agent.agent_actions(type, status); CREATE INDEX CONCURRENTLY idx_actions_input_gin ON agent.agent_actions USING GIN (input); CREATE INDEX CONCURRENTLY idx_actions_error_gin ON agent.agent_actions USING GIN (to_tsvector('english', COALESCE(error->>'message', ''))); CREATE INDEX CONCURRENTLY idx_actions_trace_id ON agent.agent_actions(trace_id) WHERE trace_id IS NOT NULL; -- 创建动作 Schema 表 CREATE TABLE IF NOT EXISTS agent.agent_action_schemas ( type TEXT PRIMARY KEY, schema JSONB NOT NULL, handler_path TEXT NOT NULL, is_system BOOLEAN DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 插入内置动作 Schema INSERT INTO agent.agent_action_schemas (type, schema, handler_path, is_system) VALUES ('noop', '{"input": {}, "output": {}}', '/handlers/noop.ts', true), ('sleep', '{"input": {"ms": {"type": "number"}}, "output": {}}', '/handlers/sleep.ts', true);提示:分区表必须在
CREATE TABLE时指定PARTITION BY RANGE,之后不能修改。建议按月分区,兼顾查询效率与管理成本。pg_partman扩展会自动创建新分区,但首次需手动初始化。
4.2 TypeScript 项目结构与核心依赖
我们采用标准的 Node.js + TypeScript 项目结构,关键依赖如下:
// package.json { "dependencies": { "pg": "^8.11.3", // PostgreSQL 客户端 "zod": "^3.22.4", // 运行时类型校验 "dotenv": "^16.4.5", // 环境变量 "uuid": "^9.0.1", // UUID 生成 "pg-pool": "^3.6.1" // 连接池 }, "devDependencies": { "@types/node": "^20.11.26", "typescript": "^5.3.3", // 注意:TS 5.3+ 支持更严格的类型推导 "ts-node": "^10.9.2", "prisma": "^5.10.0" // 可选:用于生成类型,但我们更倾向手动定义 } }项目目录结构:
src/ ├── types/ # 全局类型定义 │ ├── action.ts # BaseAction 等核心接口 │ └── session.ts # SessionContext 等 ├── lib/ # 核心逻辑 │ ├── action-factory.ts # 动作工厂与注册 │ ├── action-executor.ts # 动作执行器(含事务) │ ├── agent-runtime.ts # Agent 运行时(数据库轮询) │ └── db.ts # PostgreSQL 连接池封装 ├── handlers/ # 具体动作处理器 │ ├── fetch-order-history.ts │ ├── validate-payment.ts │ └── send-notification.ts ├── server.ts # HTTP 服务入口(可选) └── index.ts # Agent 启动入口关键配置文件(.env):
# PostgreSQL 连接 DB_HOST=your-rds-endpoint DB_PORT=5432 DB_NAME=agentdb DB_USER=agent_user DB_PASSWORD=your_secure_password # 连接池配置 PG_MAX_CONNECTIONS=50 PG_IDLE_TIMEOUT_MS=30000 PG_ACQUIRE_TIMEOUT_MS=10000 # Agent 配置 AGENT_RUNTIME_POLL_INTERVAL_MS=100 AGENT_DEFAULT_TIMEOUT_MS=300004.3 启动一个完整的 agent-native 服务
以下是一个最小可行的index.ts,展示如何串联所有组件:
// src/index.ts import { Pool } from 'pg