01

认识 Dexter

一个在终端里自主思考、自主行动的 AI 金融研究员

Dexter 是什么?

想象你有一个金融分析师助手——不是那种只会回答"建议关注"的聊天机器人,而是一个能自己自主规划研究步骤、调用真实金融数据、检查自己结论的自主代理

🧠

智能任务规划

自动把复杂问题拆成多个子任务,按顺序逐步执行——就像一个有经验的分析师会自己列研究清单

🔍

自主执行

选择并调用正确的工具获取金融数据——收入报表、资产负债表、现金流量表,全都能查

自我验证

检查自己的工作成果,发现数据矛盾时会追问到底,直到结论站得住脚

📈

实时金融数据

接入真实市场数据源——股票价格、SEC 文件、公司财报、内部人交易,不是编的

Dexter 的整体架构

整个系统像一座精密的研究工厂,数据从工具进来,经过 Agent 的思考和验证,最终产出结论。

用户层

💻
CLI / WhatsApp

Agent 核心

🔄
Agent Loop
📓
Scratchpad
🤖
LLM Engine

工具层

📊
get_financials
💰
get_market_data
📄
read_filings
🌐
web_search / browser
点击任意组件,查看它的职责说明

Dexter 的"灵魂"

Dexter 不是一个通用 AI 套了金融壳。它有一份叫 SOUL.md 的文件,定义了自己的投资哲学和人格:

SOUL.md
## Who I Am

I'm Dexter. A financial research agent
who lives in a terminal.

## How I Think About Investing

From Buffett, I carry these convictions:
- Price is what you pay,
  value is what you get.
- The best investment is a wonderful
  business at a fair price

From Munger, I carry these disciplines:
- Invert, always invert.
- Simplicity over cleverness.
中文解读

Dexter 的身份声明:它是一个住在终端里的金融研究代理

它不只是一个搜索工具——它有自己的投资价值观

巴菲特的原则:价格是你付出的,价值是你得到的

好公司合理价格 > 差公司便宜价格——质量会复利增长

芒格的纪律:永远反过来想——先问"什么会导致失败"

简单胜过聪明——如果几句话说不清投资逻辑,说明理解不够深

💡
核心理念

SOUL.md 会被注入到每次对话的系统提示(system prompt)中。这意味着 Dexter 在每一次回答中都会体现这些价值观——不是偶尔,而是始终如一。

项目目录结构

Dexter 用 TypeScript 写成,运行在 Bun 运行时上,界面用 Ink(React for CLI)构建。

src/ 全部源代码
agent/ Agent 核心:循环、提示词、Scratchpad、token 计数
agent.ts主循环——迭代调用 LLM + 工具
scratchpad.ts研究笔记——记录每次工具调用
prompts.ts系统提示词构建器
compact.ts上下文压缩——防止对话太长爆掉
tools/ 所有工具:金融、搜索、浏览器、文件系统、记忆
finance/金融数据工具:财务报表、价格、SEC 文件、选股
search/网页搜索:Exa、Tavily、Perplexity 多引擎
browser/Playwright 浏览器——爬取 JS 渲染页面
model/ LLM 多模型抽象层
skills/ 可扩展技能:DCF 估值、X 研究
memory/ 持久化记忆:向量搜索 + 时间衰减

检验你的理解

Dexter 和普通金融聊天机器人(比如直接问 ChatGPT 股票问题)最本质的区别是什么?

如果你想改变 Dexter 的投资风格(比如让它更激进),你应该修改哪个文件?

02

Agent 核心循环

Dexter 如何像人类研究员一样思考、行动、验证

想象一座侦探事务所

Dexter 的 Agent Loop 就像一个侦探接手案件后的工作流程:接到问题 → 列出需要查的线索 → 去现场取证 → 回来分析 → 发现新线索 → 再去查 → 直到真相大白。

1
接收问题

用户输入:"苹果公司现在估值合理吗?"——这个复杂问题需要多步研究

2
LLM 思考下一步

AI 模型分析:"要判断估值,我需要先查财务数据,再算 DCF 估值"

3
调用工具

调用 get_financials 拿到苹果 5 年现金流,调用 get_market_data 获取当前股价

4
验证与迭代

拿到数据后,LLM 发现缺少负债信息,继续调用工具补充——直到数据充分

5
输出结论

不再需要调用工具时,LLM 基于所有数据给出最终分析——这就是 done 事件

Agent Loop 的核心代码

下面是 agent.ts 中主循环的简化版本,每一步都有清晰的注释:

agent.ts
async *run(query: string) {
  // 最多循环 10 次
  while (ctx.iteration < 10) {
    ctx.iteration++;
    // 1. 调用 LLM(流式)
    response = await streamLlm(messages);

    // 2. 没有工具调用 = 最终答案
    if (!hasToolCalls(response)) {
      yield { type: 'done', answer };
      return;
    }

    // 3. 执行工具(可并发)
    toolMessages = yield* executeTools(response);
    messages.push(response, ...toolMessages);
  }
}
逐行解读

用 async generator 实现——每一步都能向外发送事件(思考中、工具调用中...)

最多迭代 10 次——防止 Agent 陷入无限循环

每轮先调用 LLM:给模型看历史对话,让它决定下一步

关键判断:如果 LLM 不再请求工具,说明它准备好给出最终答案了

输出 done 事件,包含最终答案和所有工具调用记录

如果 LLM 请求了工具,就执行它们

执行结果追加到消息历史中,下一轮 LLM 就能看到工具返回的数据

一次完整研究的数据流

当用户问"苹果公司估值合理吗?"时,数据如何在各组件间流转:

👤
用户
🔄
Agent Loop
🔧
工具集
点击"下一步"开始动画

Scratchpad——研究笔记本

每次查询都会创建一个 Scratchpad 文件(JSONL 格式),记录 Agent 的所有工作。这是 Dexter 区别于"黑箱 AI"的关键——每一步都有据可查。

scratchpad.jsonl
{"type":"init",
 "content":"苹果估值合理吗?"}
{"type":"tool_result",
 "toolName":"get_financials",
 "args":{"ticker":"AAPL",
   "period":"annual","limit":5},
 "result":{...}}
{"type":"thinking",
 "content":"收入增长强劲,但需检查负债"}
逐行解读

init:记录用户的原始问题

tool_result:记录一次完整的工具调用

包括工具名、参数、和返回的完整数据

thinking:LLM 的中间推理过程

帮助用户理解 Agent 为什么做了某些决策

📋
防循环机制

Scratchpad 内置了工具调用计数和查询相似度检测。如果同一个工具被调用 3 次以上,或者重复发送相似查询,它会发出警告引导 LLM 换一个方向。不会强制阻止——只是温柔地提醒:"嘿,你好像在原地打转。"

实时事件流

Agent Loop 用 AsyncGenerator 向外发送事件,CLI 界面能实时显示 Agent 在做什么:

工具并发执行

Dexter 有一个聪明的优化:只读工具(查询数据)可以并发执行,写入类工具(修改文件)必须排队。

tool-executor.ts
private partitionToolCalls(toolCalls) {
  for (const call of toolCalls) {
    const isSafe =
      concurrencyMap.get(call.name);
    if (isSafe) {
      // 合并到并发批次
      batch.calls.push(call);
    } else {
      // 单独串行执行
      batches.push({ concurrent: false });
    }
  }
}
逐行解读

把 LLM 请求的一批工具调用分成"并发安全"和"需排队"两组

查询每个工具的 concurrencySafe 标记

只读工具(如 get_financials)标记为安全,可以并行跑

写入工具(如 write_file)标记为不安全,必须排队

这就像快递员同时送多个包裹 vs 排队取钱

检验你的理解

Agent Loop 如何判断"该输出最终答案了"?

如果 LLM 同时请求了 get_financials(AAPL) 和 write_file(report.md),Dexter 会怎么执行?

03

金融工具箱

Dexter 连接真实金融世界的 14 把"瑞士军刀"

工具注册表

Dexter 的所有工具都在 registry.ts 中注册。每个工具携带三个信息:工具实例、详细描述、是否安全可并发。

📊
get_financials

智能元工具——输入自然语言问题,自动路由到收入报表、资产负债表等子工具

💰
get_market_data

股票/加密货币价格、公司新闻、内部人交易——多资产查询一次搞定

📄
read_filings

SEC 文件阅读器——10-K 年报、10-Q 季报、8-K 事件报告,自动提取关键段落

🔍
stock_screener

条件选股器——按市盈率、增长率、利润率等指标筛选股票

🌐
web_search + browser

搜索网页 + Playwright 浏览器——搜索只是开始,还能打开页面提取全文

get_financials:工具里的工具

最强大的工具是 get_financials。它不是一个工具,而是"工具路由器"——用一个 LLM 路由器把自然语言查询翻译成具体的数据请求。

get-financials.ts
// 1. 用户自然语言查询
input: { query: "苹果5年现金流" }

// 2. LLM 路由到正确的子工具
const { response } = await callLlm(
  input.query, {
    systemPrompt: buildRouterPrompt(),
    tools: FINANCE_TOOLS  // 子工具列表
});

// 3. 并行执行子工具
const results = await Promise.all(
  toolCalls.map(tc => tool.invoke(tc.args))
);
逐行解读

用户不需要知道技术细节,只要用自然语言描述想要什么

用另一个 LLM 调用来"翻译"查询——决定该调用哪个子工具

路由提示词包含所有子工具的描述和选路规则

FINANCE_TOOLS 是一个数组:收入报表、资产负债表、现金流表...

LLM 可能决定同时调用多个子工具(比如同时查现金流和负债)

所有子工具并行执行,结果合并返回

工具匹配挑战

把每个工具拖到对应的场景中:

get_financials
get_market_data
read_filings
stock_screener
browser

"比较苹果和微软近3年营收"

拖到这里

"特斯拉最新10-K年报中关于AI战略的部分"

拖到这里

"英伟达当前股价和今天的新闻"

拖到这里

"找出市盈率低于15、营收增长超20%的公司"

拖到这里

"打开彭博社文章,提取全文内容"

拖到这里

安全机制:工具审批

写文件和编辑文件是敏感操作。Dexter 会在执行前弹出确认提示,让用户决定是否允许:

tool-executor.ts
const TOOLS_REQUIRING_APPROVAL =
  ['write_file', 'edit_file'];

if (requiresApproval(toolName)
    && !sessionApproved.has(toolName)) {
  const decision = await
    requestApproval({tool, args});
  if (decision === 'allow-session') {
    sessionApproved.add(toolName);
  }
}
逐行解读

只有写文件和编辑文件需要审批——查数据不需要

检查这个工具是否需要审批,以及用户是否已经"会话级授权"

弹出确认框,让用户决定:允许一次、允许整个会话、拒绝

allow-session = 本次对话中这类工具都自动批准,不再询问

上下文管理:防止"记忆爆掉"

每次工具调用返回的数据可能很大。如果对话太长,token 数会超出模型限制。Dexter 有三层保护机制:

Microcompact

每轮轻量裁剪:清除旧 AI 消息中的思考文本,只保留工具调用结构。省 token 但不丢关键信息。

📦

Compaction

当 token 接近阈值时,用 LLM 把所有工具结果压缩成一段摘要,替换原始数据。像把一沓笔记浓缩成一页总结。

Truncation

最后防线——直接删除最早的消息轮次,只保留最近 3 轮。粗暴但有效。

⚠️
为什么这很重要?

如果 Agent 做了 8 次工具调用,每次返回几百行 JSON,上下文可能膨胀到几十万 token。没有压缩机制,研究到一半就会因为超出模型限制而崩溃。这是所有 Agent 系统都要面对的核心挑战。

检验你的理解

get_financials 和其他工具相比,有什么独特的架构特点?

Agent 进行了 7 次工具调用,token 数正在逼近模型上限。Dexter 的处理顺序是?

04

LLM 多模型引擎

Dexter 如何同时支持 7 家 AI 供应商,并智能选择最佳模型

7 家 AI 供应商,一个统一接口

Dexter 的 model/llm.ts 是一个翻译层——把不同的 AI 供应商统一成一套接口。就像万能充电器,不管手机品牌都能充。

O

OpenAI(默认)

默认模型 gpt-5.5。通过 API key 前缀自动识别,无需手动指定供应商。

A

Anthropic

Claude 系列模型。特殊优化:用 cache_control 实现提示缓存,大幅降低成本。

G

Google

Gemini 系列模型。通过 langchain/google-genai 适配。

D

DeepSeek

支持思考模式的国产模型。自动检测 thinking model 并启用推理模式。

X

xAI / Ollama / Moonshot

Grok、本地模型、月之暗面——覆盖云端到本地全场景。

模型路由:前缀匹配

Dexter 用模型名称的前缀来自动判断该用哪个供应商。用户只需要输入模型名,不用手动选供应商:

llm.ts
const MODEL_FACTORIES = {
  anthropic: (name, opts) =>
    new ChatAnthropic({
      model: name, ...opts,
      apiKey: getKey('ANTHROPIC_API_KEY')
    }),
  deepseek: (name, opts) => {
    const isThink = name.includes('pro');
    return new ChatOpenAI({
      model: name, ...opts,
      apiKey: getKey('DEEPSEEK_API_KEY'),
      baseURL: 'https://api.deepseek.com',
      ...(isThink && {
        extraBody: {
          thinking: { type: 'enabled' }
        }
      })
    });
  },
};
逐行解读

MODEL_FACTORIES 是一个字典——每个供应商对应一个工厂函数

Anthropic:用 ChatAnthropic 类,从环境变量读取 API key

DeepSeek:特殊处理思考模式

检测是否是支持思考的模型(如 deepseek-v4-pro)

复用 OpenAI 的客户端(因为 DeepSeek API 兼容 OpenAI 格式)

指定 DeepSeek 的 API 地址

如果是思考模型,在请求体中额外启用 thinking 模式

Anthropic 提示缓存:省 90% 的钱

系统提示(SOUL.md + 工具描述 + 记忆上下文)在每次对话中都不变。Anthropic 支持缓存这些不变部分,后续调用直接复用缓存:

llm.ts - Anthropic 缓存
function buildAnthropicMessages(
  systemPrompt, userPrompt
) {
  return [
    new SystemMessage({
      content: [{
        type: 'text',
        text: systemPrompt,
        cache_control: {
          type: 'ephemeral'
        }
      }]
    }),
    new HumanMessage(userPrompt)
  ];
}
逐行解读

构建 Anthropic 格式的消息数组

系统消息用特殊的结构化格式

把文本内容包在对象里

关键:标记 cache_control 为 ephemeral(临时缓存)

Anthropic 服务端会缓存这段系统提示

下一次调用时如果系统提示没变,直接命中缓存

输入 token 成本降低约 90%——这在 Agent 循环中意义重大

💡
为什么这很重要?

Agent Loop 每轮迭代都会发送完整的消息历史(包括系统提示)。一次研究可能触发 5-10 轮迭代。没有缓存,同样的系统提示会被重复计费 5-10 次。有了缓存,只有第一次全价,后续 90% 折扣。这是 Agent 应用的成本关键。

流式响应:让用户看到"思考过程"

Dexter 使用 流式响应,让用户实时看到 Agent 在做什么——思考中、调用工具中、生成答案中:

📤
发送请求
📥
接收流
执行
点击"下一步"观看流式响应的生命周期

重试机制:指数退避

AI 供应商偶尔会出错(限流、超时、服务中断)。Dexter 用指数退避策略自动重试:

llm.ts - withRetry
async function withRetry<T>(
  fn: () => Promise<T>,
  provider: string,
  maxAttempts = 3
): Promise<T> {
  for (let attempt = 0;; attempt++) {
    try { return await fn(); }
    catch (e) {
      if (isNonRetryable(e))
        throw; // 认证错误等不重试
      await sleep(500 * 2 ** attempt);
    }
  }
}
逐行解读

泛型函数——适用于任何需要重试的异步操作

记录供应商名称,用于错误日志

默认最多尝试 3 次(1次原始 + 2次重试)

执行实际操作

如果出错,先判断是否值得重试

认证错误(API key 无效)不值得重试——直接抛出

等待 500ms * 2^attempt:0.5s → 1s → 2s

找 Bug 挑战

下面这段 LLM 调用代码有一个潜在问题,你能找到吗?

1 const result = await callLlm(query, { model });
2 const toolCalls = result.response.tool_calls;
3 for (const tc of toolCalls) { // process }
4 return { toolCalls, answer: result.response };

检验你的理解

Dexter 使用 Anthropic 模型时,有一个特殊的成本优化措施。它是什么?

如果用户切换到 ollama:llama3 模型,Dexter 如何知道该用本地 Ollama 而不是 OpenAI?

05

记忆与可扩展技能

让 Dexter 记住你是谁,并且学会新的研究技能

Dexter 的记忆系统

Dexter 没有跨会话记忆——每次对话从零开始。但它有一个持久化记忆系统,能记住你的偏好、投资风格和重要事实:

记忆系统架构

记忆存储在 .dexter/memory/ 目录下,分为长期记忆和每日日志。检索时使用向量搜索 + MMR 多样性算法:

💾
memory_update

写入记忆:追加到长期记忆或每日日志。支持编辑和删除已有条目。

🔎
memory_search

语义搜索记忆内容和历史对话。用向量匹配 + 时间衰减,越近期的记忆权重越高。

📖
memory_get

读取指定记忆文件的特定行范围。用于精确查看之前存储的内容。

可扩展技能系统

Dexter 的技能不是写死在代码里的——它们是 SKILL.md 文件。每个技能就是一个 Markdown 文档,描述了完成某个任务的步骤清单:

skills/dcf/SKILL.md
---
name: dcf-valuation
description: Performs discounted cash
  flow (DCF) valuation
---

# DCF Valuation Skill

## Step 1: Gather Financial Data
Call `get_financials` with:
- "[TICKER] annual cash flow"
- "[TICKER] financial metrics"
- "[TICKER] latest balance sheet"

## Step 2: Calculate FCF Growth
5-year CAGR from cash flow.
Cap at 15% (sustained higher
growth is rare).
逐行解读

YAML 前置元数据:技能名称和触发描述

当用户问"苹果估值多少"、"DCF 分析"时自动触发

Markdown 正文:详细的步骤清单

第一步:收集数据——查现金流、财务指标、资产负债表

每一步都指定了该调用哪个工具、用什么参数

第二步:计算自由现金流增长率

设上限 15%——因为持续超高增长在现实中很罕见

技能的发现与注入

启动时,Dexter 扫描 skills/ 目录下所有 SKILL.md 文件,把它们的元数据注入系统提示。LLM 看到技能描述后,会在需要时自动调用:

prompts.ts - buildSkillsSection
function buildSkillsSection() {
  const skills = discoverSkills();
  if (skills.length === 0) return '';

  return `## Available Skills
${buildSkillMetadata()}
## Skill Usage Policy
- When relevant, invoke IMMEDIATELY
- Each skill runs at most once
  per query`;
}
逐行解读

扫描 skills/ 目录发现所有 SKILL.md 文件

没有技能时不注入(节省 token)

把技能元数据格式化为系统提示的一部分

告诉 LLM:如果技能相关,立即调用,不要犹豫

关键规则:每个技能每次查询最多执行一次——防重复

DCF 估值技能完整流程

当用户问"苹果值多少钱"时,DCF 技能会自动触发,执行一个 8 步流程:

📊
数据收集
🔢
计算分析
🎯
估值建模
验证输出
点击"下一步"走一遍 DCF 估值流程

系统提示的组装

所有这些组件——工具描述、技能元数据、记忆上下文、SOUL.md 身份——最终被组装成一个完整的系统提示:

Current Date 当前日期——确保 LLM 知道"今天"是哪天
Tool Descriptions 所有工具的简短描述和使用策略
Skill Metadata 可用技能的名称和触发条件
Memory Context 用户的偏好、历史投资决策等个性化信息
User Rules 用户自定义的研究规则(如"只看市值超100亿的公司")
SOUL.md Dexter 的身份、投资哲学和人格
Channel Profile 输出通道配置——CLI 用表格,WhatsApp 用简洁文本

最终检验

如果你想给 Dexter 添加一个新的"竞争对手分析"技能,你需要做什么?

当 Dexter 开始一次新对话时,系统提示中包含以下哪些组件?

Dexter 的 memory_search 检索记忆时,用到了什么技术?