认识 Dexter
一个在终端里自主思考、自主行动的 AI 金融研究员
Dexter 是什么?
想象你有一个金融分析师助手——不是那种只会回答"建议关注"的聊天机器人,而是一个能自己自主规划研究步骤、调用真实金融数据、检查自己结论的自主代理。
智能任务规划
自动把复杂问题拆成多个子任务,按顺序逐步执行——就像一个有经验的分析师会自己列研究清单
自主执行
选择并调用正确的工具获取金融数据——收入报表、资产负债表、现金流量表,全都能查
自我验证
检查自己的工作成果,发现数据矛盾时会追问到底,直到结论站得住脚
实时金融数据
接入真实市场数据源——股票价格、SEC 文件、公司财报、内部人交易,不是编的
Dexter 的整体架构
整个系统像一座精密的研究工厂,数据从工具进来,经过 Agent 的思考和验证,最终产出结论。
用户层
Agent 核心
工具层
Dexter 的"灵魂"
Dexter 不是一个通用 AI 套了金融壳。它有一份叫 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)构建。
检验你的理解
Dexter 和普通金融聊天机器人(比如直接问 ChatGPT 股票问题)最本质的区别是什么?
如果你想改变 Dexter 的投资风格(比如让它更激进),你应该修改哪个文件?
Agent 核心循环
Dexter 如何像人类研究员一样思考、行动、验证
想象一座侦探事务所
Dexter 的 Agent Loop 就像一个侦探接手案件后的工作流程:接到问题 → 列出需要查的线索 → 去现场取证 → 回来分析 → 发现新线索 → 再去查 → 直到真相大白。
用户输入:"苹果公司现在估值合理吗?"——这个复杂问题需要多步研究
AI 模型分析:"要判断估值,我需要先查财务数据,再算 DCF 估值"
调用 get_financials 拿到苹果 5 年现金流,调用 get_market_data 获取当前股价
拿到数据后,LLM 发现缺少负债信息,继续调用工具补充——直到数据充分
不再需要调用工具时,LLM 基于所有数据给出最终分析——这就是 done 事件
Agent Loop 的核心代码
下面是 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 就能看到工具返回的数据
一次完整研究的数据流
当用户问"苹果公司估值合理吗?"时,数据如何在各组件间流转:
Scratchpad——研究笔记本
每次查询都会创建一个 Scratchpad 文件(JSONL 格式),记录 Agent 的所有工作。这是 Dexter 区别于"黑箱 AI"的关键——每一步都有据可查。
{"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 有一个聪明的优化:只读工具(查询数据)可以并发执行,写入类工具(修改文件)必须排队。
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 会怎么执行?
金融工具箱
Dexter 连接真实金融世界的 14 把"瑞士军刀"
工具注册表
Dexter 的所有工具都在 registry.ts 中注册。每个工具携带三个信息:工具实例、详细描述、是否安全可并发。
智能元工具——输入自然语言问题,自动路由到收入报表、资产负债表等子工具
股票/加密货币价格、公司新闻、内部人交易——多资产查询一次搞定
SEC 文件阅读器——10-K 年报、10-Q 季报、8-K 事件报告,自动提取关键段落
条件选股器——按市盈率、增长率、利润率等指标筛选股票
搜索网页 + Playwright 浏览器——搜索只是开始,还能打开页面提取全文
get_financials:工具里的工具
最强大的工具是 get_financials。它不是一个工具,而是"工具路由器"——用一个 LLM 路由器把自然语言查询翻译成具体的数据请求。
// 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 可能决定同时调用多个子工具(比如同时查现金流和负债)
所有子工具并行执行,结果合并返回
工具匹配挑战
把每个工具拖到对应的场景中:
"比较苹果和微软近3年营收"
"特斯拉最新10-K年报中关于AI战略的部分"
"英伟达当前股价和今天的新闻"
"找出市盈率低于15、营收增长超20%的公司"
"打开彭博社文章,提取全文内容"
安全机制:工具审批
写文件和编辑文件是敏感操作。Dexter 会在执行前弹出确认提示,让用户决定是否允许:
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 的处理顺序是?
LLM 多模型引擎
Dexter 如何同时支持 7 家 AI 供应商,并智能选择最佳模型
7 家 AI 供应商,一个统一接口
Dexter 的 model/llm.ts 是一个翻译层——把不同的 AI 供应商统一成一套接口。就像万能充电器,不管手机品牌都能充。
OpenAI(默认)
默认模型 gpt-5.5。通过 API key 前缀自动识别,无需手动指定供应商。
Anthropic
Claude 系列模型。特殊优化:用 cache_control 实现提示缓存,大幅降低成本。
Gemini 系列模型。通过 langchain/google-genai 适配。
DeepSeek
支持思考模式的国产模型。自动检测 thinking model 并启用推理模式。
xAI / Ollama / Moonshot
Grok、本地模型、月之暗面——覆盖云端到本地全场景。
模型路由:前缀匹配
Dexter 用模型名称的前缀来自动判断该用哪个供应商。用户只需要输入模型名,不用手动选供应商:
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 支持缓存这些不变部分,后续调用直接复用缓存:
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 用指数退避策略自动重试:
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 调用代码有一个潜在问题,你能找到吗?
const result = await callLlm(query, { model });
const toolCalls = result.response.tool_calls;
for (const tc of toolCalls) { // process }
return { toolCalls, answer: result.response };
检验你的理解
Dexter 使用 Anthropic 模型时,有一个特殊的成本优化措施。它是什么?
如果用户切换到 ollama:llama3 模型,Dexter 如何知道该用本地 Ollama 而不是 OpenAI?
记忆与可扩展技能
让 Dexter 记住你是谁,并且学会新的研究技能
Dexter 的记忆系统
Dexter 没有跨会话记忆——每次对话从零开始。但它有一个持久化记忆系统,能记住你的偏好、投资风格和重要事实:
记忆系统架构
记忆存储在 .dexter/memory/ 目录下,分为长期记忆和每日日志。检索时使用向量搜索 + MMR 多样性算法:
写入记忆:追加到长期记忆或每日日志。支持编辑和删除已有条目。
语义搜索记忆内容和历史对话。用向量匹配 + 时间衰减,越近期的记忆权重越高。
读取指定记忆文件的特定行范围。用于精确查看之前存储的内容。
可扩展技能系统
Dexter 的技能不是写死在代码里的——它们是 SKILL.md 文件。每个技能就是一个 Markdown 文档,描述了完成某个任务的步骤清单:
---
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 看到技能描述后,会在需要时自动调用:
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 步流程:
系统提示的组装
所有这些组件——工具描述、技能元数据、记忆上下文、SOUL.md 身份——最终被组装成一个完整的系统提示:
Current Date
当前日期——确保 LLM 知道"今天"是哪天
Tool Descriptions
所有工具的简短描述和使用策略
Skill Metadata
可用技能的名称和触发条件
Memory Context
用户的偏好、历史投资决策等个性化信息
User Rules
用户自定义的研究规则(如"只看市值超100亿的公司")
SOUL.md
Dexter 的身份、投资哲学和人格
Channel Profile
输出通道配置——CLI 用表格,WhatsApp 用简洁文本