跳到主要内容

Dexter — 架构与原理

30 秒导读: Dexter 是一个用 TypeScript 写的自主金融研究 agent——你用大白话问它一个投资/财报问题,它自己规划、调工具取实时市场数据、检查自己的结果、必要时反复迭代,最后给一个有数据支撑的答案。作者把它定位成「金融版 Claude Code」。技术上它就是一个不断增长的消息数组 + 读工具并发执行 + 三级上下文压缩的流式循环,再加上跨会话的向量记忆、子 agent 委派,以及能挂在 WhatsApp / cron 上无人值守跑。


1. 这是什么(零基础也能懂)

一句话定义: Dexter 是一个跑在终端里的 AI agent,专门做深度金融研究——把一个复杂问题拆成步骤、自己选工具、自己取数据、自己核对,直到有把握。

解决什么问题 / 给谁用。 假设你想搞清楚"过去 5 年 NVDA 和 AMD 谁的毛利率扩张更快、原因是什么"。这需要拉两家公司多年的利润表、算增长率、可能还要读财报和新闻。手动做要开十几个页面。Dexter 让你一句话问出去,它在后台自动完成整条研究链,最后把结论和数据出处一起给你。它面向想用 agent 做投研的工程师(注意:项目 README 明确声明仅供学习娱乐,不是投资建议)。

它能做什么(核心能力):

  • 把复杂问题拆成研究步骤,自主选工具执行
  • 实时金融数据:利润表 / 资产负债表 / 现金流表、关键比率、财报、分部、内幕交易、股价、加密货币、新闻
  • 自我校验:检查自己的中间结果,不够就再取数据
  • 安全护栏:循环检测、步数上限、工具调用上限,防止跑飞
  • 跨会话记忆:把你说过的偏好、财务目标记下来,下次还认得你
  • 可挂在 WhatsApp 上聊,也可以定时"心跳"盯盘,满足条件才主动推送

用起来什么样: 装好 Bun、配好 API key,一行命令进交互模式:

bun start
# 然后直接问:Compare NVDA vs AMD gross margin over the last 5 years

它会一边流式打印思考,一边并发拉数据,最后给出带出处的答案。所有工具调用还会落到 .dexter/scratchpad/*.jsonl 便于回溯(见 README「How to Debug」)。

一句话直觉: 把 Dexter 想成一个只会用金融数据 API 的 Claude Code——同样是「LLM 主循环 + 一堆工具 + 上下文管理」,只是工具换成了财报和行情,并且额外长了「记忆」和「无人值守」两条腿。

本节不谈代码。记住一件事:它本质是个 agent 循环,不是一个财经数据网站。


2. 顶层全景(它大概怎么转)

Dexter 的入口是 src/index.tsx(bun start),默认模型 gpt-5.6-sol,主循环上限 10 轮(src/agent/agent.ts:23-24,DEFAULT_MODEL / DEFAULT_MAX_ITERATIONS)。整个系统可以拆成两个层面:一次 query 怎么跑完,以及围绕它的持久设施(记忆、无人值守)。

2.1 一次 query 的主线(怎么读:从上往下是一轮迭代内的顺序,循环回到顶部直到出答案)

用户 query


┌──────────────────────────────────────────────┐
│ Agent.run() 主循环 (最多 10 轮) │
│ │
│ ① 微压缩 microcompact:清掉旧工具结果正文 │
│ ② 流式调 LLM(失败→阻塞式兜底) │
│ ③ 没有工具调用? → 这就是最终答案,返回 │
│ ④ 有工具调用 → 交给工具执行器 │
│ ├─ 权限闸门:allow / ask / deny │
│ └─ 读工具并发跑,写工具串行 + 审批 │
│ ⑤ 大结果落盘、按预算截断,塞回消息数组 │
│ ⑥ 上下文超阈值? → 记忆flush→LLM摘要→硬截断 │
│ ⑦ 排空用户新消息队列,回到 ① │
└──────────────────────────────────────────────┘
│ (工具执行时)

元工具 get_financials / get_market_data ...
→ 内部再调一次 LLM,把自然语言路由到底层数据工具 → 并发取数

主线走一遍(高层,不进代码):query 进来 → 组装成 [系统提示, 历史, 用户消息] 数组 → 每轮先微压缩再问 LLM → LLM 要么直接回答、要么发起工具调用 → 工具执行器按「读并发/写串行」执行并过权限闸门 → 结果塞回数组 → 若上下文太大就压缩 → 下一轮 → 直到 LLM 不再调工具,那次输出即最终答案。

2.2 持久设施(跨 query、跨会话)

┌─────────────┐ ┌──────────────────┐
│ 记忆系统 │ │ 无人值守/交付 │
│ SQLite 混合 │ │ WhatsApp 网关 │
│ 向量检索 │◀──────▶│ + Cron 心跳 │
└─────────────┘ └──────────────────┘
▲ │
│ 压缩前 flush durable │ 定时/来消息时
│ facts;每轮可检索 ▼
Agent.run() 主循环 ────────▶ 启动一个隔离 agent 跑

2.3 部件一句话职责

部件干什么在哪个文件
Agent 主循环增长消息数组、流式调 LLM、编排工具与压缩src/agent/agent.ts
工具执行器把工具调用分成并发/串行批,过权限闸门src/agent/tool-executor.ts
元工具用一次 LLM 调用把自然语言路由到底层数据工具src/tools/finance/get-financials.ts
微压缩每轮清掉旧只读工具结果的正文src/agent/microcompact.ts
全量压缩用快模型把所有工具结果摘成结构化 summarysrc/agent/compact.ts
记忆系统SQLite 混合检索 + 时间衰减 + MMRsrc/memory/
权限引擎allow/ask/deny,bash 命令解析分类src/permissions/engine.ts
子 agent隔离委派子任务,可一轮并行多个src/tools/subagent/spawn-subagent.ts
Skill从 SKILL.md 加载专项工作流指令src/tools/skill.ts
网关WhatsApp 收发、路由、群 @ 门控src/gateway/gateway.ts
Cron/心跳定时唤醒隔离 agent,满足条件才推送src/cron/runner.tssrc/cron/executor.ts

3. 阅读地图(建议顺序)

这是一套多文件文档。推荐从核心循环读起,再逐层深入——上下文工程(第 3 章)是全书精华,值得重点看。

  1. Dexter 全景:金融版 Claude Code 是怎么转的 —— 你正在读的这篇,建立整体心智模型。
  2. 核心主循环:一次 query 如何端到端跑完 —— Agent.run() 逐步拆解:消息数组、流式+阻塞兜底、迭代上限、溢出重试、消息队列排空。
  3. 工具层:元工具路由、并发批处理与权限闸门 —— 「元工具用二次 LLM 路由」的设计、读工具并发批、权限引擎 allow/ask/deny 与 bash 解析。
  4. 上下文工程:让上下文有界又不丢信息(全书精华) —— 三级压缩(微压缩→LLM 摘要→硬截断)、大结果落盘、结果预算、剥离旧思考。
  5. 记忆系统:SQLite 混合检索 + 时间衰减 + MMR —— 向量+关键词混合打分、指数时间衰减、MMR 去冗余、压缩前记忆 flush。
  6. 委派与专长:子 agent 并行研究与 Skill 工作流 —— 隔离子 agent 的边界与并行、SKILL.md 加载与去重。
  7. 无人值守与多渠道交付:WhatsApp 网关与 Cron 心跳 —— 群 @ 门控、会话内消息注入、cron 调度、心跳抑制与退避。

4. 巧妙之处(读者要带走的精华)

这一节先白话点出「妙在哪」,再给源码锚点。这些是 Dexter 值得借鉴的工程决策。

① 三级上下文压缩,越贵越晚用。 不是简单「满了就删」,而是分梯度:每轮先做微压缩(把旧的只读工具结果正文换成 [Old tool result content cleared] 占位,免费、无损语义上下文结构),超过阈值再做LLM 全量摘要(把所有工具结果压成结构化 summary,信息保留但花一次 LLM 调用),摘要失败或结果太少才硬截断旧轮次。微压缩触发条件是可压缩工具消息 >8 条(保留最近 4 条)或累计 >80k tokens(src/agent/microcompact.ts:17-30)。妙在把「省钱的手段」排在「花钱的手段」前面,大多数长会话靠免费的微压缩就撑住了。

② 元工具:用二次 LLM 调用做「自然语言 → 数据源」路由。 get_financials 对主 agent 只暴露一个"传自然语言 query"的口子,内部再起一次 LLM(绑了 8 个底层财务工具)专门做路由:解析公司名→ticker、推断日期区间、选对的报表工具,再并发执行(src/tools/finance/get-financials.ts:129-186,createGetFinancials)。妙在主 agent 的工具列表被极大收窄——它只需知道"要财务数据就调这个",复杂的 ticker/日期/工具选择被下沉到一个专职小 LLM,主上下文不被十几个细粒度工具 schema 撑爆。

③ 读工具并发、写工具串行,靠"连续同类合批"。 执行器把一轮里的工具调用按 concurrencySafe 切成批:连续的只读工具合成一个并发批(上限 10 路)一起跑,遇到写工具就单独串行并走审批(src/agent/tool-executor.ts:94-126,partitionToolCalls)。妙在保序——结果最终仍按原始 tool_calls 顺序拼回(src/agent/agent.ts:418-425),LLM 看到的顺序稳定,但取数被并行加速。

④ 记忆检索是"混合打分 → 时间衰减 → MMR 去冗余"的流水线。 向量分(0.7)+ 关键词分(0.3)加权合并,再乘一个指数时间衰减(半衰期 30 天,但 MEMORY.md 这类常青文件豁免),最后用 MMR(最大边际相关,λ=0.7,Jaccard 相似度)把内容雷同的候选剔掉,才取 top-K(src/memory/search.ts:31-145,hybridSearch)。妙在同时对抗三种失效:关键词漏召回、旧记忆霸榜、返回一堆近乎重复的片段。

⑤ 压缩前先"抢救"durable facts 进记忆。 当上下文逼近阈值、马上要压缩时,先跑一次 memory flush:让 LLM 从当前会话里挑出"值得长期记住的用户偏好/财务目标"写进当天的记忆文件,做压缩(src/agent/agent.ts:573-597 + src/memory/flush.ts:39-68)。妙在压缩会丢细节,但真正重要的用户画像在丢之前已经落库,下次会话还能检索回来。

⑥ 无人值守时用"哨兵 token"抑制噪音。 cron 心跳跑完后,如果没有可报告的事,agent 被要求只回一个 HEARTBEAT_OK_TOKEN;执行器识别到这个 token 就静默、不推送(src/cron/executor.ts:132-136,158-168)。妙在把"没事发生"变成可机器识别的信号,用户只在真有情况时被打扰,避免定时任务变成骚扰。

⑦ 权限的 session 缓存 key 精确到"具体命令"。 批准一次 bash 命令的 allow-session 授权,key 是该命令原文而非工具名(src/permissions/engine.ts:127-135,sessionKey)。妙在批准 git status 绝不会顺带放行 git push --force——避免"一次批准、全程通行"的安全塌陷。


5. 代码地图(导航索引)

一张跳转表:想深入哪个机制就打开对应文件、grep 对应符号。符号名比行号抗漂移。

主题文件路径关键符号
Agent 主循环src/agent/agent.tsAgentrunmanageContextThresholdstripOldThinking
流式+阻塞兜底src/agent/agent.tscallModelWithStreamingstreamAndAccumulateinspectChunkContent
工具执行/并发src/agent/tool-executor.tsAgentToolExecutorexecuteAllpartitionToolCallsexecuteBatchConcurrently
工具注册表src/tools/registry.tsgetToolRegistrygetToolConcurrencyMapbuildCompactToolDescriptions
元工具路由src/tools/finance/get-financials.tscreateGetFinancialsbuildRouterPromptFINANCE_TOOLS
微压缩src/agent/microcompact.tsmicrocompactMessagesCOMPACTABLE_TOOLSCOUNT_TRIGGER_THRESHOLD
全量压缩src/agent/compact.tscompactContextbuildCompactionPromptformatCompactSummary
压缩阈值src/utils/tokens.tsgetAutoCompactThresholdestimateTokensKEEP_TOOL_USES
记忆管理器src/memory/index.tsMemoryManagersearchloadSessionContext
混合检索src/memory/search.tshybridSearchnormalizeWeights
时间衰减src/memory/temporal-decay.tsapplyTemporalDecaycalculateTemporalDecayMultiplierisEvergreenFile
MMR 去冗余src/memory/mmr.tsapplyMMRToHybridResultsjaccardSimilaritytokenize
记忆 flushsrc/memory/flush.tsrunMemoryFlushshouldRunMemoryFlushMEMORY_FLUSH_TOKEN
权限引擎src/permissions/engine.tsevaluatePermissionevaluateBashsessionKey
子 agentsrc/tools/subagent/spawn-subagent.tscreateSpawnSubagentSPAWN_SUBAGENT_DESCRIPTION
Skill 加载/调用src/skills/loader.tssrc/tools/skill.tsparseSkillFileskillTool
WhatsApp 网关src/gateway/gateway.tsstartGatewayhandleInbound
Cron 调度src/cron/runner.tsstartCronRunnertickscheduleNext
Cron 执行src/cron/executor.tsexecuteCronJobisWithinActiveHourserrorBackoffMs
心跳检查src/gateway/heartbeat/prompt.tsbuildHeartbeatQueryHEARTBEAT_OK_TOKEN
Scratchpadsrc/agent/scratchpad.tsScratchpadToolCallRecordDEFAULT_LIMIT_CONFIG

说明: 本文档锁定 commit 823052e,所有引用 as-of 该提交。分章文件(01–06)将各机制展开为密集的代码走读。若上游更新未动这些符号,结论仍成立;动了但仅行号漂移,按符号名重新定位即可。