项目 / PROJECT

Web3 链上智能 Agent 助手

基于 FastAPI、LangGraph 与 PostgreSQL 的链上 Agent:模型负责语义理解与结构化交互,工具权限、条件求值、交易构造与恢复由代码约束。通过可中断的确认流程、上下文预算与离线评估,让执行过程可控、结果可验证。

2026.10.26 独立开发 · Agent 链路与全栈
  • FastAPI
  • LangGraph
  • PostgreSQL
  • React 19
  • TypeScript

#项目背景

自托管钱包的用户自己保管私钥,也自己承担每一笔交易的后果。常见需求分为两类:查询资产余额、交易记录、质押信息与协议规则,以及发起转账。这些操作分散在不同入口:钱包需要填写交易表单,区块浏览器展示链上记录,协议规则则需要查阅文档。

这个助手把查询与转账整合到同一个对话入口。说「我 USDC 还剩多少」,直接得到对应链上的资产余额;说「给 0x… 转 50 U」,得到的是一张参数齐全的待签名卡。用户不必先判断该开哪个页面,也不必把意图自行拆成表单字段。查询与写入共用一条入口,写操作还必须由用户亲手确认,系统不代签。

链上操作的代价是真实的:转账一旦确认,系统无法替用户撤回;收款地址或金额填错,损失也无法靠重新提问挽回。项目据此明确职责边界:模型负责语义理解、参数抽取与回答生成,路由、条件求值、工具执行、记忆生命周期与交易核验由代码约束;签名与广播交给用户钱包,私钥不经过系统;链上数据回答以成功的工具调用为依据,知识问答以本轮检索证据为依据。

#项目角色

独立完成全栈开发,重点负责 Agent 链路:结构化意图抽取与纯函数路由、只读取数循环、条件转账计划与八态状态机、工具注册与证据门、文档入库与检索、上下文预算与长期记忆,以及四套离线评估集。前端围绕后端流程状态组织交互,涵盖钱包登录、SSE 进度展示、确认卡、交易状态轮询与知识库管理;会话与任务数据由后端统一持久化。

#技术方案

#整体架构

Web3 链上智能 Agent 助手整体架构:双图分流,写路径在确认卡处停下

请求自上而下经过四层 —— 接口、编排、工具与链适配;持久化独立在一侧,由各层共用,职责互不重叠。模型调用集中在编排层:它提出只读工具调用,但看不见写工具;执行层再按本轮意图校验工具白名单。四个关键判断的分工如下:

判断由谁做理由
这句话是什么意图、缺哪个参数结构化 schema 抽取 + 纯函数路由路由要能单测与回归;模型输出结构化字段,分支由代码选择
余额够不够、条件成不成立代码,根据原始整数值与 decimals 比较资产数量条件成立与否决定是否建卡,不能采信模型给出的「成立」
出卡前能发现哪些交易风险参数、网络、白名单、精度、限额、余额与 eth_call 预演七类检查把已识别的参数错误与执行失败尽量拦在出卡前,减少无效签名与失败交易
链上到底成没成回执内容核对(from / to / value / input / 日志)status=1 只说明没回滚,不代表收款人与金额符合预期

查询与写操作使用两条执行路径。查询与知识问答进入只读图,模型推理最多 5 步。资产与质押按需取数,交易记录确定性调用并直接渲染,知识问答先检索再生成;取数失败时返回明确的降级说明。条件转账进入五节点确定性计划,节点职责与执行顺序由代码固定。

#方案对比

选择替代方案理由
LangGraph + PG 检查点手写状态机 / AgentExecutor挂起与恢复是原生能力,检查点落库才能在另一条 HTTP 请求里续跑;挂起节点在恢复时会从头执行,因此建任务等业务写入放在独立的前置节点(见难点二)
双图(ReAct + 确定性序列)一张 ReAct 图跑到底写操作的下一步不能由模型决定:同一句条件指令多循环一轮,就可能多出一张待确认的卡
单 Agent + 代码分流多 Agent 分工(意图、查询、写链各起一个)意图范围固定为 10 种,路由由纯函数实现;单 Agent 配合代码分流,便于统一校验与回归,也减少了模型间的信息传递与协调成本
意图抽取用结构化 schema + 代码路由让模型自行决定调哪个工具模型负责参数抽取,代码负责分支选择,路由可单测、可回归
上下文按调用路径分级注入每轮拼一份全量材料记忆分两档:意图阶段只给硬约束型,软记忆在只读执行阶段按需补充,让有限上下文优先容纳当前任务所需的信息
人工确认卡插在写路径中间一次生成后直接执行先构造并持久化待确认任务,再回显参数;用户确认后才开放钱包签名与广播。prepare 建单,gate 挂起,确认恢复不重复建单
自建评估集(四套用例,RAG 两层分跑)改完抽查几条样例命中率、召回、MRR 可复算,可对比参数调整前后的表现,也能识别「命中但答不全」这类问题
PostgreSQL + pgvector 单库独立向量库 + Redis 队列入库任务通过 PG 租约机制领取(FOR UPDATE SKIP LOCKED),减少独立服务的部署与维护成本
SSE 阶段事件WebSocket / 一次性 JSON单轮对话只需由服务端推送进度;幂等键处理重复提交,状态查询补齐断线后的任务状态
模型可配置更换写死单一模型模型名、地址、温度、上下文窗口都在配置里,业务层只依赖聊天接口与结构化输出契约;换模型只动适配器那一层,判据不变,重跑同一批用例就能对比

意图抽取方案经过了基线测试与回归验证。早期版本用模型自报的 confidence 做澄清判断,阈值 0.75。跑完 52 条工具用例与 15 条澄清用例的基线后发现这个分数难以区分是否需要澄清:33 条计分用例里 32 条在 0.9 以上,阈值几乎不触发;而判错的 5 条里,4 条是参数齐全、意图也判对了的写操作指令 —— 它既拦不住该澄清的,也会让参数完整的指令进入澄清分支。删掉 confidence 分支,改用显式的澄清标志加一条确定性判据(剥离动作词与语气词后是否还剩查询对象),复测:信息不足组 0/9 → 7/7(7 条计分全过,另 2 条是合约白名单未配的已知缺口、不计分),明确组误澄清保持 0/6,计分路由 29/33 → 30/33。

#核心业务

入口与阶段流:一次请求一条 SSE 流,帧只有三类 —— stage 报进度、result 返回整轮结果、error 返回流开始后的错误;result.type 实际产出 answer、ask、intent_confirm 三种。前端生成请求幂等键,后端另存内容摘要用于一致性校验:相同请求重复提交时回放已有结果;同一幂等键对应的请求内容发生变化时返回 E512;请求仍在执行时返回 E511。

意图与路由:意图空间是 10 值闭合枚举(4 读、4 写、闲聊、越界),模型按结构化 schema 抽取结果,纯函数将请求分配到 read、write、clarify、reject 四条分支;路由结果决定是否追问、是否进入写路径,并确定执行层允许调用的工具范围。chainId 与付款钱包身份由服务端注入;交易记录查询绑定登录钱包,收款地址从用户指令中抽取并在确认卡回显。

只读路径:资产、交易记录、质押三个工具共用只读执行图,模型推理步数上限 5;交易记录直接调用工具并确定性渲染,避免长列表被模型改写。需要链上证据却完全没有工具调用时,移除没有数据依据的回答,要求模型先调用工具,再补跑一次;已有调用但未取得有效证据时,返回明确的失败说明。

写路径与确认:单步转账由确定性写链路构造;包含读取或条件判断的转账计划进入五节点图 collect → judge → afford → prepare → gate;只有 prepare 创建业务任务,gate 只挂起(顺序为何如此见难点二)。签名广播后前端回报哈希,服务端按 from、to、value、input 与日志核对回执。主要状态流转为 INTENT_CONFIRM → PREVIEW → SUBMITTED → SETTLED;过期分两类,未广播的任务可按原参数重新发起;已广播但链上结果未知的任务则禁止直接重发。

上下文装配:四层材料按路径注入:意图识别与只读执行阶段均可使用档案和摘要,记忆分档 —— 意图抽取只带硬约束,意图识别完成后,在适用的只读执行路径追加软记忆,知识问答轮不追加。历史预算从最新消息向前分配,优先裁掉最旧内容,只要历史预算大于零,就保留最新一条消息;长期记忆作为独立的背景信息块注入,与系统规则分开。

知识问答:文档入库涵盖上传、任务领取、切块、bge-m3 向量化与审核发布;检索时并行查询向量索引与关键词索引快照,加权 RRF 融合(1.0 / 0.6,k=60),top_k=4、距离上限 0.46,重排默认关闭。引用按 S1、S2 编号,快照随消息落库,恢复历史不重新检索。

人工确认与状态同步:图在 gate 挂起,前端拿到的只是一张确认卡 —— 卡片不推断状态,后端八种状态通过统一映射转换为卡片展示,前端以服务端返回的任务状态为准;卡片展示与 prepare 保存的交易参数来自同一份数据,前端不重算金额与地址。签名前再读一次状态,已过期就拦下;广播后上报的哈希只记录、不推进状态,状态由下一次回执核对写回。

失败与降级:错误按来源分层 —— 模型输出不合结构、工具执行失败、数据源不可用、链上不可用、模型服务不可用,各自对应固定文案与状态码。重试按失败类型区分:模型输出结构不符合要求时修复一次,漏调工具时补跑一次;RPC 与区块浏览器对传输异常或 HTTP 5xx 最多重试一次,确定性参数错误不重试。检索的空结果与降级是两种状态,前者是本次检索未找到符合条件的证据,后者是检索服务未能完成查询,不共用一句「未找到」。

身份与配额:SIWE 挑战、签名核验换 JWT,nonce 由仓储签发并核销,一次一用。require_identity 是对话、工作流、知识库三类接口的统一前置依赖;限流与配额由 QuotaController 统一管理,按分钟窗口计数,已认证请求按用户统计,未认证请求按 IP 统计;后台记忆抽取使用独立的内部配额,不消耗用户交互额度。

可观测性:一轮对话一条 trace,根节点在入口建立,意图抽取、图节点、工具调用、记忆抽取与摘要均关联到这条调用链。关键纯 Python 决策通过审计事件补齐;LangSmith run_id 由后端生成并随审计记录落库,事后可通过该 ID 查询完整调用树。追踪开关统一管理,关闭时同步将环境变量设为 false —— 否则 shell 里残留的 true 会让上层继续埋点,开关就不再是唯一入口。追踪上报失败不阻断业务请求,避免观测服务故障影响主流程。

#难点与解决方案

三处难点分别落在读、写、检索三条链路上:读不能编、写不能重复执行、检索不能靠手感。

#难点一:写工具对模型不可见,靠断言而不是提示词

问题:模型能在零次工具调用下给出余额或「已转账」,措辞与真答案一致。无论是没有调用工具、调用全部失败,还是调用了不相关的工具,模型都可能返回一段通顺的中文。仅看回答,很难发现背后的执行问题。把「不要编造」写进提示词不构成边界,绕过时不报错。

解决方案:工具可见性通过注册分组控制,并用断言检查配置。写工具标记为 mutating,注册到仅状态机可用的分组,取工具列表时若它出现在可见组,直接 raise —— 过滤掉之后模型确实看不见,但配置写错没人知道;断言让工具注册错误立即暴露。来源校验:非状态机发起的写调用一律抛 ToolSourceViolation;交易记录查询绑定当前登录钱包;转账付款地址与条件计划的取数地址由服务端注入,收款地址从用户指令中抽取,并在确认卡中回显 —— 让身份地址受代码约束,目标地址可供用户核对。取数结果则由证据校验把关:对于需要数据的意图,如果执行轨迹中没有对应工具的成功记录,就用失败说明替换整段回答 —— 模型输出的是带地址和金额的一整段,追加会变成「编的」和「真的」两段并存。

结果:模型知道工具名也进不了有副作用的路径,这是代码保证,不是概率;缺少证据的回答不会原样进入历史,避免被后续轮次当成事实引用。

#难点二:interrupt 的恢复会重跑节点,副作用必须放在恢复点之前

问题:恢复执行时,LangGraph 会从挂起节点的开头重新运行。建任务写在 gate 里,用户点一次确认就多一张待签名的卡;只在同一个节点里把建任务挪到 interrupt 之前,也仍会在恢复重跑时再次触发。关键不是把代码行前移,而是把建任务拆到独立的前置节点,让待确认任务先落库、挂起节点只负责等待用户选择。

解决方案:

collect(查询链上数据,自动补齐条件判断所需的查询)
   ↓
 judge ──不成立 / 数据不足──> END(不建任务)
   ↓ 成立
afford(只做余额比较)
   ↓
prepare(构造 + 建任务,唯一有副作用的节点)
   ↓
 gate(interrupt 挂起,不写业务状态)

五个节点中,prepare 负责创建任务,gate 只调用 interrupt 等待确认。确认恢复时任务已在库中,重跑 gate 不会再次建任务;副作用与挂起节点分离,让这条恢复路径可以直接验证。节点拆分与业务幂等共同约束重复执行,同时增加两道业务约束:建卡前按自然键(用户、钱包、链、收款地址、币种、金额)复用未结案任务;确认接口先推进状态机再 resume。前端在签名前再次读取任务状态,已过期就直接拦下 —— 避免用户继续发送已经过期的任务;已结算、失败或拒绝的任务再上报哈希,按状态冲突拒收;过期但实际已广播的任务仍允许补报哈希并继续核对链上结果。

结果:条件不成立时不建任务;确认恢复只重跑无业务写入的 gate,重复确认与页面恢复复用已有任务。节点拆分处理挂起恢复,业务幂等处理重复建单,状态机处理重复确认,三层约束共同控制写操作的执行与恢复。

#难点三:把“找到相关材料”和“回答正确”分开评估

问题:检索参数之间存在取舍。放宽相似度门槛,有助于中文问题找到英文文档,但也容易引入主题相关、实际却没有答案的材料;增加返回条数,又可能超出回答模型的上下文预算。仅凭几次问答,很难判断一次调整究竟改善了什么。尤其要区分两种情况:一是找到了部分依据,回答却漏掉了关键要点;二是检索结果看起来相关,却根本无法回答用户的问题。

解决方案:将检索与回答分开评估。检索层检查包含答案的文档片段能否进入结果,统计首位命中率(hit@1)、前三位命中率(hit@3)、召回率与平均倒数排名(MRR);答案层沿用生产提示词生成回答,再检查要点是否完整、语言是否正确,以及材料不足时是否明确拒答。

分层评估还能帮助识别评分本身的偏差。例如,一道事实题预先标注的目标片段没有进入前 5 名,模型却仍然答对了:同一事实也出现在其他片段中。因此,既要检查检索排名,也要检查实际返回的证据,不能只凭最终答案判断检索质量。

在固定用例上对比后,最终采用距离阈值 0.46、最多返回 4 个片段、关键词融合权重 0.6 的配置。同时排除了两项看似合理、实际效果更差的改动:

  • 过滤高频关键词:WBERA、sWBERA、BERA 在单协议文档中频繁出现,但恰好是区分相关问题的关键。按词频过滤后,这类问题反而更难找到正确材料。
  • 预算不足时截断长片段:截断后的长片段占满剩余预算,挤掉了后续包含答案的片段,导致事实题的前三位命中率下降约 9 个百分点,因此保留跳过超长片段、继续选择后续材料的策略。

结果:8 道改写问法的题目,正确片段排在首位的数量从 6 道提升到 8 道;8 道中文提问检索英文文档的题目,前三位命中数量从 6 道提升到 7 道。5 道多事实与跨片段题中,3 道找齐所需材料,另外 2 道找到部分依据。

更关键的发现是,9 道资料中没有答案的问题,都检索到了主题相关的材料。例如,询问某个池子当前的 APY,返回的却是解释 APY 概念的文档。部分无答案样本的向量距离为 0.328–0.398;这个值越小,通常表示问题与材料越相似,但它们的距离与真正包含答案的材料存在重叠。也就是说,“主题相似”并不能证明“材料足以回答问题”,单靠调整距离阈值无法可靠区分两者。因此,回答阶段还需要依据本轮材料判断证据是否充分,资料不足时明确拒答。

这套评估基于一个协议的 43 个文档片段,为距离阈值、返回条数、融合权重与上下文预算建立了固定的对比基线。后续更换模型或调整参数时,可以复测同一批问题,分别观察检索排序、证据完整性与回答质量的变化。

#项目成果

  • 行为约束:写工具不进入模型上下文,调用来源按状态机校验;无据回答整段替换为失败说明,对回答中的数字与地址做启发式证据检查,全部无法匹配时整体降级
  • 可中断与可恢复:确认卡处挂起、检查点落库、在另一条 HTTP 请求里续跑;确认恢复不重复执行建任务节点,重复确认与页面恢复复用已有任务
  • 上下文:四层材料分层装配,记忆按硬约束与软记忆两档注入;超预算优先裁剪最旧历史,历史预算大于零时保留最新一条消息,长期记忆作为独立背景块注入,与系统规则分开;会话版本 + 租约 + 水位线保证清空后不被写回;记忆抽取后台运行、失败退避、同主题去重
  • 离线评估:工具选择 52 条(按路由、工具与槽位分别评分)、澄清判定 15 条、多跳计划 9 条、RAG 46 条;RAG 检索层改写问法 8 条 hit@1 全中(原 6 条)、中文问英文文档 hit@3 从 6/8 到 7/8,在固定语料与本地 qwen2.5:7b 基线下,答案层 46 条规则判定全过、36 条要点全中、9 条无答案题全部拒答
  • 可复现:检索层与答案层各一条命令重跑,采用固定用例与评分口径对比;46 条用例、评分脚本与基线文档随仓库提交,换模型或改参数都在同一批题上比
  • 工具与安全:七类检查涵盖参数、网络、白名单、精度、限额、余额与 eth_call 预演,单笔美元等值限额按配置价格折算,出卡前通过余额检查与预演拦截已识别的失败;回执结合交易内容与 ERC20 Transfer 日志核对,确认、拒绝、广播、结算与两类过期状态由同一套状态机统一管理
  • 交付规模:22 个 HTTP 接口声明(含健康检查)、16 张数据表、9 个仓储、3 个只读工具加 1 个知识检索工具(启用时注册)、1 个写工具;前端 5 个页面、25 个组件;后端约 1.49 万行 Python 另配 1 万行测试,前端 5 千行 TypeScript
  • 工程质量:523 条单元测试通过,另配 79 条数据库集成测试,覆盖持久化、并发与状态迁移

#项目链接

项目截图 / SCREENSHOTS

截图1
01 截图1
截图2
02 截图2
截图3
03 截图3
截图4
04 截图4
截图5
05 截图5
双图分流:只读 ReAct 循环与五节点确定性计划,写路径在确认卡处停下
06 双图分流:只读 ReAct 循环与五节点确定性计划,写路径在确认卡处停下
一句话进入系统后的三次判断:意图抽取、纯函数路由、按意图收窄工具白名单
07 一句话进入系统后的三次判断:意图抽取、纯函数路由、按意图收窄工具白名单
确认卡之前的七道检查,任一道不通过都不出卡
08 确认卡之前的七道检查,任一道不通过都不出卡
八态流转:从用户确认、钱包签名到链上回执核对
09 八态流转:从用户确认、钱包签名到链上回执核对
四层上下文按调用路径装配:意图识别使用硬约束记忆,只读执行阶段按需补充软记忆
10 四层上下文按调用路径装配:意图识别使用硬约束记忆,只读执行阶段按需补充软记忆

返回作品集