价格助手实战:从一次提问理解 Agent 流程与前后端分工

输入“获取当前 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,其中包含 symbolpricepriceTypesourcefetchedAt。这样,交易所响应格式不会直接扩散到整个页面。

第五步:工具结果必须交回模型

拿到行情后,服务端保留模型原来的 assistant 消息,并追加与调用 ID 对应的 tool 消息。实际代码中的关键结构是:

messages.push({
  role: 'tool',
  tool_call_id: call.id,
  content: JSON.stringify(data),
})

tool_call_id 相当于请求关联号:模型通过它知道这份数据回答的是哪一次工具调用。如果执行失败,回填的内容是明确的错误码,而不是一份虚构行情。

服务端随后带着完整消息序列再次请求 DeepSeek。模型读到真实工具结果,才能据此组织说明。如果模型不再返回工具调用,这次循环就结束;如果还需要工具,Runner 会在预算内继续执行。

第六步:前端把数据和说明分别展示

服务端响应包括 statusanswerdatatraceusage。它们分别表示任务状态、文字说明、结构化行情、执行记录和模型用量。

价格卡片直接读取 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.tsHTTP 输入边界与任务入口
lib/price-agent/prompt.tstools.ts模型规则、工具说明和运行时参数校验
lib/price-agent/runner.ts消息历史、工具调用 ID、循环与停止条件
lib/price-agent/deepseek.tsmarket.ts模型和行情服务的适配层
lib/price-agent/response.ts浏览器收到响应后的结构校验

阅读时可以先从 route.ts 找到 runAgent,再跟到 validateToolexecuteToolmessages.push。这几处连接起来,就是“理解需求、提出调用、执行工具、读取结果、结束任务”的完整 Agent 流程。

体验价格助手