输入“获取当前 BTC 价格”,页面最终显示一个价格。对于前端开发者,最熟悉的实现是点击按钮后调用固定接口。那么,加上 Agent 之后,中间到底多了什么?
本站价格助手让模型把自然语言转换成工具调用请求,再由服务端执行确定的业务代码。模型负责理解问题和组织说明,行情接口负责提供价格,前端负责把结果变成可读的卡片和图表。
本文按照仓库中的实际实现,拆开这条交互链路。可以先打开价格助手,再对照执行记录阅读。
一、这个助手能做什么
| 功能 | 可以怎样提问 | 实际行为 |
|---|---|---|
| 查询现价 | 获取当前 BTC 价格 | 查询 BTC/USDT 当前最优卖价,展示来源、单位和抓取时间 |
| 查询历史 | 查询以太坊最近七天价格 | 查询 ETH/USDT 最近七个完整 UTC 自然日的收盘价 |
| 组合查询 | 查询 SOL 当前价格和最近七天走势 | 在一次任务中查询同一币种的现价与历史 |
| 图表与明细 | 使用历史查询结果 | 展示折线图、日期范围、逐日价格表和缺失日期 |
| 执行记录 | 查询后展开记录 | 查看模型请求、参数校验、工具执行、回答阶段及耗时 |
当前支持 BTC、ETH、SOL,以 USDT 报价。“比特币”“以太坊”“Solana”等名称由模型根据共享配置中的别名映射到交易对。
页面上的币种按钮用来切换快捷问题,不是独立的查询参数。如果选中了 ETH 按钮,但输入框写的是“获取 SOL 价格”,实际查询仍以文字为准。
这里的“当前价格”采用 Binance bookTicker 返回的 askPrice,即最优卖价,作为买入参考报价。它不是成交保证价,fetchedAt 也是本服务抓取数据的时间,不是交易所生成报价的时间。结果仅用于行情查询和工程演示。
目前一次请求只支持一个币种,没有账户余额查询、最大购买量计算或下单功能。例如“查询当前 BTC 价格以及我的余额,看我最多能买多少 BTC”,其中账户和购买量部分超出了当前工具范围,不能假装已经完成。
二、先把 Prompt、Tool Call 和 Agent 分清楚
可以把这三个概念对应到熟悉的前端开发过程:
| 概念 | 在本项目中的含义 | 前端可以怎样理解 |
|---|---|---|
| Prompt | 用户问题,以及服务端提供的角色、范围和回答规则 | 给任务补充需求与约束 |
| Tool Definition | 工具名称、用途和参数结构 | 向模型公开的接口说明 |
| Tool Call | 模型返回的工具名称、参数和调用 ID | 一份待校验、待执行的结构化请求 |
| Tool Result | 服务端执行工具得到的数据或错误 | 业务函数返回值 |
| Agent Runner | 管理模型请求、工具执行、结果回填和结束条件的程序 | 负责整个任务的控制流程 |
Tool Call 本身不执行代码。 模型返回 getCurrentPrice,不代表它已经访问了 Binance;只有服务端校验并运行对应函数后,价格查询才真正发生。
如果产品只有一个固定币种的查询按钮,直接请求行情接口就能完成需求。这个助手加入模型,是为了把不同表达映射到现价、历史或组合查询,并根据工具结果给出解释。代价是增加模型调用的等待时间和用量。
三、跟着“获取当前 BTC 价格”走一遍
下面是一次正常查询的交互。图中的两次模型请求,发生在同一次浏览器请求内部。
用户输入“获取当前 BTC 价格”
↓
浏览器 POST /api/price-agent
↓
服务端检查配置、请求来源、输入与运行限额
↓
第一次请求 DeepSeek:规则 + 用户问题 + 工具定义
↓
模型返回 getCurrentPrice 的 Tool Call
↓
服务端校验工具与参数 → 请求 Binance → 整理行情数据
↓
第二次请求 DeepSeek:原消息 + 工具调用 + 对应工具结果
↓
模型给出说明,服务端返回回答、数据与执行记录
↓
浏览器校验响应 → 展示价格卡片与说明
第一步:浏览器只提交问题和语言
客户端提交的业务数据很小:
{
"message": "获取当前 BTC 价格",
"locale": "zh"
}
前端没有上传模型 Key,也没有让用户指定 system 消息、工具列表或行情 URL。浏览器只请求本站 /api/price-agent;DeepSeek 和 Binance 的调用都发生在 Next.js 服务端。
接口会检查功能是否开启、模型 Key 是否存在、JSON 格式、问题长度和语言。当前公开版本不要求访问口令;如果请求带有 Origin,服务端会检查它是否与本站一致。来源检查不能代替身份认证。
第二步:服务端告诉模型有哪些工具
prompt.ts 提供角色、支持币种、UTC 时间、回答语言和业务边界。比如:价格问题要使用工具,不能根据记忆编造报价;不明确的币种应先询问。
tools.ts 另外提供工具定义。现价工具的参数结构相当于:
{
"type": "object",
"properties": {
"symbol": {
"type": "string",
"enum": ["BTCUSDT", "ETHUSDT", "SOLUSDT"]
}
},
"required": ["symbol"],
"additionalProperties": false
}
工具定义告诉模型“可以提出什么调用”,运行时校验则保证程序“实际允许什么调用”。即使参数约束已经发给模型,服务端仍然会再次检查。
第三步:模型返回待执行的调用
下面是一次可能的工具调用结构,ID 仅为说明用途:
{
"id": "call_price_1",
"type": "function",
"function": {
"name": "getCurrentPrice",
"arguments": "{\"symbol\":\"BTCUSDT\"}"
}
}
注意 arguments 是 JSON 字符串。服务端先解析它,再检查工具名是否在白名单内、symbol 是否受支持、是否夹带了额外字段。不存在根据模型文字执行任意函数的步骤。
“模型分析”在这里指模型选择了工具与参数。页面展示的是这个选择后产生的执行记录,不会展示模型内部推理。
第四步:真正执行行情请求
通过校验后,executeTool 将调用分发到 getCurrentPrice。市场适配器发出如下路径的请求:
GET /api/v3/ticker/bookTicker?symbol=BTCUSDT
返回值还要检查币种是否匹配、价格格式是否有效。成功后整理成应用自己的 quote,其中包含 symbol、price、priceType、source 和 fetchedAt。这样,交易所响应格式不会直接扩散到整个页面。
第五步:工具结果必须交回模型
拿到行情后,服务端保留模型原来的 assistant 消息,并追加与调用 ID 对应的 tool 消息。实际代码中的关键结构是:
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(data),
})
tool_call_id 相当于请求关联号:模型通过它知道这份数据回答的是哪一次工具调用。如果执行失败,回填的内容是明确的错误码,而不是一份虚构行情。
服务端随后带着完整消息序列再次请求 DeepSeek。模型读到真实工具结果,才能据此组织说明。如果模型不再返回工具调用,这次循环就结束;如果还需要工具,Runner 会在预算内继续执行。
第六步:前端把数据和说明分别展示
服务端响应包括 status、answer、data、trace 和 usage。它们分别表示任务状态、文字说明、结构化行情、执行记录和模型用量。
价格卡片直接读取 data.quote.price,历史图表直接读取 data.history.points。前端不从模型回答里用正则提取价格,也不把模型生成的 HTML 当作界面执行。
因此,模型负责解释数据,业务组件负责展示已校验的数据。即使总结阶段失败,已经取得的有效行情仍有机会以部分成功的结果展示出来。
四、历史查询和组合查询怎样变化
“查询最近七天 ETH 价格”会映射到:
{
"name": "getPriceHistory",
"arguments": {
"symbol": "ETHUSDT",
"days": 7
}
}
这里展示的是解析后的参数。当前 days 只接受 7;具体日期由服务端计算,不由模型自行填写。
例如在 2026-07-13 UTC 发起请求,查询的是 07-06 至 07-12 的七个完整日收盘价,不包含 07-13,也不是向前滚动 168 小时。市场适配器请求 /api/v3/klines,使用日线周期和截止日前一毫秒作为结束时间。
如果缺少某天的数据,响应会列出 missingDates,图表保留断点,不补零或编造价格。
“查询 SOL 当前价格和最近七天走势”需要两个工具。模型可以在一次响应里提出两个调用,当前执行器按顺序执行,并分别用调用 ID 回填结果;也可能分轮提出调用,但仍受总次数限制。
如果一次请求涉及多个币种,Prompt 会要求模型先让用户选择一个;即使模型仍提出混合调用,服务端也会拒绝。共享配置支持三个币种,不代表一次响应支持三币对比。
五、前后端分别负责什么
| 工作 | 浏览器前端 | Next.js 服务端 |
|---|---|---|
| 接收需求 | 输入框、快捷问题、字符计数、语言选择 | 校验消息与语言,只接受约定字段 |
| 管理交互 | 查询中状态、防止重复提交、取消请求、错误提示 | 控制任务超时、调用次数和执行顺序 |
| 理解问题 | 提交原始问题 | 组织 Prompt 和工具定义,调用 DeepSeek |
| 执行业务 | 展示返回的数据 | 校验 Tool Call、执行行情工具、检查返回值 |
| 密钥配置 | 不持有 DeepSeek Key | 读取服务端环境变量,在模型请求中使用 Key |
| 渲染结果 | 校验响应,展示卡片、ECharts、明细表和执行记录 | 返回稳定的数据结构、状态和脱敏错误码 |
项目里的“服务端”就是 Next.js Route Handler 和 lib/price-agent,可以随同博客部署,不要求另建一个后端仓库。对前端开发者而言,新增的主要工作是服务端编排与校验;React 状态管理、异步请求和组件展示仍然是熟悉的部分。
当前请求使用 stream: false。页面会在等待期间显示查询中,完成后一次性拿到执行记录;这些记录不是实时推送的进度。
每次提问也都是独立任务,不保存上一轮用户对话。用户看到澄清问题后,应重新提交包含币种和查询需求的完整问题;“那 ETH 呢”这样的上下文追问没有历史消息可依赖。一次任务内部的多轮模型调用,与多轮聊天是两件事。
六、失败、取消和公开访问的边界
当前 Runner 最多进行 3 次模型请求、4 次工具执行,每次模型请求最多输出 800 token。它会预留读取工具结果的模型轮次,避免最后一轮仍执行无法交回模型的工作。
整体任务超时为 45 秒,单次模型请求 20 秒,单次行情请求 8 秒。前端取消会中断浏览器 fetch,服务端通过请求信号传递取消;部署代理是否及时报告断开取决于运行环境,已发出的模型调用也可能仍计入用量。
结果有三种状态:
completed:正常完成,也可能是模型给出的澄清说明。partial:已拿到有效行情,但后续失败,或者历史数据有缺失。failed:没有可用行情且任务失败。
公开版本不读取 PRICE_AGENT_ACCESS_TOKEN,访客无需输入口令。DEEPSEEK_API_KEY 仍保存在服务端,所有调用使用站点账户的模型额度。现有单并发与两秒间隔只在单个进程内生效,并没有实现跨实例的每 IP 限流或全站每日预算。
七、对照代码阅读
| 文件 | 建议关注的内容 |
|---|---|
config/price-agent.ts | 币种、别名、报价单位的共享配置 |
app/[locale]/price-agent/page.tsx | 服务端配置检查与页面入口 |
app/[locale]/price-agent/price-agent-client.tsx | 提交、取消、状态与结果展示 |
app/[locale]/price-agent/price-chart.tsx | 图表、缺失数据、主题响应和资源清理 |
app/api/price-agent/route.ts | HTTP 输入边界与任务入口 |
lib/price-agent/prompt.ts、tools.ts | 模型规则、工具说明和运行时参数校验 |
lib/price-agent/runner.ts | 消息历史、工具调用 ID、循环与停止条件 |
lib/price-agent/deepseek.ts、market.ts | 模型和行情服务的适配层 |
lib/price-agent/response.ts | 浏览器收到响应后的结构校验 |
阅读时可以先从 route.ts 找到 runAgent,再跟到 validateTool、executeTool 和 messages.push。这几处连接起来,就是“理解需求、提出调用、执行工具、读取结果、结束任务”的完整 Agent 流程。