1. 架构定位:为什么把 JDGenie 设计成“智能体编排中枢”
接手这个项目之前,我先说一下背景。很多做电商运营或商品研究的人,手上都会堆一堆“半自动”工具——有的是浏览器插件,有的是按键脚本,有的是简单爬虫脚本。这些工具解决单个问题还可以,一旦需要跨多个平台、多个环节协作,比如“先比价,再查历史价格,再算到手价,再按条件生成采购清单”,你就得来回切换工具、手动传递数据,整个过程又慢又容易出错。
JoyAgent-JDGenie 的出发点就是把这一整套流程收拢到一个统一的智能体系统里。系统级架构的定位不是“又写了一个爬虫脚本”,也不是“给浏览器加了个辅助面板”,而是一个可扩展的智能体编排中枢:负责接收用户自然语言指令,拆解任务,调度内部各个工具模块,汇总结果,再通过可读性强的报告反馈给用户。
从系统设计的角度,JDGenie 可以拆成四个层级来理解:
| 层级 | 职责 | 对应模块 |
|---|---|---|
| 交互层 | 接收指令、展示结果 | 聊天式输入框、结果卡片、任务进度条 |
| 编排层 | 理解意图、规划步骤、调度模块 | 指令解析器、任务队列、状态机管理 |
| 执行层 | 具体干活、返回结构化数据 | 比价模块、历史价格模块、订单导入模块等 |
| 数据层 | 存配置、存任务记录、导出报表 | 本地配置库、任务日志、CSV导出器 |
这种分层的核心目的是隔离变化。比如交互层想从命令行改成 Web 页面,不影响执行层;以后新增一个“查物流”的模块,只需要在执行层注册新能力,编排层不需要改动。这种低耦合的设计,让系统在没有完整产品团队的情况下也能逐模块迭代,任何一个模块出了问题都可以单独替换或降级处理。
JDGenie 适合谁?如果你经常做商品对比、需要整理大量商品数据、想用自然语言驱动这些重复操作,或者本身在研究智能体工具链的架构方案,这个系统的设计思路都值得参考。它不是一个大厂级的分布式平台,而是一个“一个人也能维护、但设计上不丢专业度”的中型智能体项目。
2. 核心架构拆解:编排层、执行层与数据层的协作方式
2.1 用户指令如何变成机器可执行任务
JDGenie 的入口很简单,就是一个类似聊天框的输入区。但背后处理链路比表面看起来复杂得多。用户的输入五花八门,比如:
- “帮我查下某品牌笔记本电脑近三个月的价格走势”
- “对比这几款手机的到手价,要拿到最优购买时机”
- “把购物车里所有商品按降价幅度排个序”
这些指令的共同点是:意图完整,但表达不结构化。编排层需要做三件事:实体识别、意图判定、任务拆分。
实体识别负责从自然语言中抽取关键参数,比如商品名称、品牌、型号、时间范围。意图判定决定这次请求是“查询类”“对比类”还是“操作类”。任务拆分把一个大目标拆成有序的子任务,比如“查价格走势”天然包含“搜索商品”“定位商品ID”“获取历史数据”“组织输出”四个步骤。
这里有个容易踩坑的细节:不要试图用一个巨大模型一次完成所有事。JDGenie 的做法是先用一个轻量级意图分类器做粗分类,再根据分类结果调用不同的解析模板。粗分类速度很快,大部分指令 200 毫秒内就能完成意图识别;只有复杂指令才升级到更重的语义解析链路。这种两级处理方式的好处是响应迅速,而且每个环节都可以单独测试和调优,不必因为改了某一类指令的解析逻辑就去重跑全量用例。
2.2 会话状态与任务状态机的设计取舍
很多智能体项目死在同一个问题上:状态管理混乱。用户上一轮说查笔记本电脑,这一轮说“那对比一下上面三款的到手价”,“上面三款”指代的是什么?如果系统不维护会话上下文,这个指令根本无法执行。
JDGenie 在会话管理上引入了一个轻量级状态机,每个会话都保存四个关键状态:
- 当前商品候选集(比如上一轮检索出来的三款笔记本)
- 已提取的实体参数(品牌、型号、价格范围、时间窗口)
- 当前任务执行进度(正在执行哪一步、哪些子任务已完成)
- 最近一次结果摘要(用于回答“和刚才比有什么变化”这类追问)
状态机本身不复杂,复杂的是状态迁移的时机。比如用户在中途突然换了话题,是清空候选集还是保留?我的设计决策是:只要实体参数发生了明显变化(比如从“笔记本电脑”变成了“手机”),就把候选集作废,防止把两类商品混在一起计算。这类规则看起来简单,但在实际使用中大幅减少了“答非所问”的问题。
另一个值得强调的设计:任务队列与状态机分离。状态机只记录“用户会话的状态”,任务队列单独管理“后台正在执行的任务”。因为有些任务耗时长,比如抓取历史价格可能需要几十秒甚至几分钟,如果状态机被一个长时间任务卡住,用户就什么都操作不了了。分离之后,用户可以在后台任务执行期间继续输入新指令,系统会判断新指令是“插入优先队列”还是“与当前任务冲突,需等待”。这种设计很像操作系统的进程调度,但用在一个智能体系统里同样合适。
2.3 数据层如何避免“本地文件越存越乱”
JDGenie 的基础数据来源是用户授权的本地浏览器会话,因此数据层天然要处理大量半结构化的页面数据。最开始我的设计是每抓到一个页面就往本地 JSON 文件里塞一份,结果一周之后文件又多又乱,排查数据全靠人肉翻文件夹。
重构之后的数据层采用三档存储策略:
- 临时缓冲层:存放最近一次任务抓取的原始页面快照,任务结束后 24 小时自动清理。
- 结构化数据库:存放商品实体、价格记录、比价结果等清洗后的数据,带有商品 ID、采集时间、来源页面等标准字段,这条数据链路的核心是为了“可追溯”。
- 导出层:用户主动触发的 CSV/Excel 报表生成,不自动落库,按需生成。
三档分离的好处是:临时数据丢了不影响全局,结构化数据始终干净,用户导出的报表又不会污染数据库。如果说这套系统有什么最值得借鉴的抽象,我觉得就是这个“数据分层”的思路,它让整个系统在数据膨胀之后仍然可以快速定位问题。
3. 执行层深挖:模块化工具链与数据采集边界
3.1 从“浏览器自动化”到“模块化工具注册”
JDGenie 的执行层最初是一个人肉堆积的脚本集合:查价格写一个函数,查历史价格又写一个函数,代码互相调用全靠手工。后来我意识到,这种写法在功能到五个以上时就完全失控了。重构后执行层改为工具注册表模式:
tools/ ├── registry.py # 工具注册与发现 ├── price_lookup/ # 实时价格查询 ├── history_trend/ # 历史价格走势 ├── deal_calculator/ # 到手价计算 └── cart_analyzer/ # 购物车商品分析每个工具模块只暴露一个统一接口,结构大致是:
class BaseTool: name = "base_tool" description = "tool description" def run(self, params: dict) -> dict: raise NotImplementedError编排层不关心工具内部怎么实现,只需要知道每个工具的名称、适用参数和返回结构。新增一个“查优惠券”工具,只要实现run方法并在注册表里声明,整个系统马上就能用上新能力——这正是模块化工具链带来的扩展性红利。
但模块化也有代价:工具之间经常需要传递数据。比如“到手价计算”需要商品原价、促销价、优惠券这几组数据,数据来自不同工具。JDGenie 的解决方案是定义了一个全局上下文对象,每个工具可以读取自己的输入参数,在结束时把结构化结果写回上下文。这有点像流水线作业,每一道工序只需要关心自己和相邻工序的接口,不需要知道整个流程的细节。
3.2 数据抓取时机的选择:为什么不能全量高频抓取
执行层涉及到一个很实际的工程问题:什么时候去抓数据。早期设计里,JDGenie 试图做到价格“实时更新”,用户每次请求都去重新抓页面。结果有两个问题:一是价格页面经常有反自动化拦截,抓取频率一高就会被限;二是许多数据其实不需要实时,比如历史价格三个月的数据,哪怕缓存一小时也不会影响最终的决策判断。
最后我采用的是分级数据新鲜度策略:
| 数据类型 | 缓存策略 | 刷新时机 |
|---|---|---|
| 商品基础信息 | 缓存 60 分钟以上 | 用户主动刷新或缓存过期 |
| 实时价格 | 每次用户发起请求时重抓,但同一商品两次请求间隔不少于 1 分钟 | 请求触发 |
| 历史价格走势 | 按天粒度缓存 | 每天首次请求时重抓 |
| 购物车信息 | 缓存 5 分钟 | 超过 5 分钟才允许重新抓取 |
这套策略落地之后,系统的请求量大概下降了 70%,但用户感知到的数据准确度几乎没变。这个经验对任何做数据采集类产品的朋友应该都有启发:不要追求数据绝对的实时,要追求在业务决策窗口内的实时。对于一个比价系统,五分钟前的价格和此刻的价格,对用户的影响几乎可以忽略;但五分钟的缓存能极大降低被拦截的风险。
3.3 页面结构变动时的兜底策略
没有哪个页面结构是永远不变的。运营方一改版,之前写好的选择器就全部失效。JDGenie 最早遇到这种情况时,我的处理方式很狼狈——手动修选择器、重新测试。后来改成了一套三层容错机制:
- 第一层:优先使用标准选择器(如商品 ID、价格类名),命中就直接提取。
- 第二层:标准选择器失效时,自动切换到备用属性匹配(如 aria-label、data-xx 这类相对稳定的自定义属性)。
- 第三层:仍然失败时,将整个页面快照存入待分析队列,并在结果中标注“该条数据可能不准确,需人工确认”。
这套机制解决的不是“百分百抓取成功”的问题,而是“失败时知道哪里失败了、失败了多少、影响面有多大”的问题。我宁可让一条数据明确标记为“不确定”,也不希望系统默默返回一个错的数据让用户做决策——这在交易场景里是底线。
4. 编排智能:任务拆解策略与多路径决策引擎
4.1 为什么任务拆解是智能体架构的核心难点
执行层的每个工具都像一个单独的手艺人:会查价的查价,会算优惠的算优惠。但如果没有人告诉他们干活的先后顺序、中间结果交给谁,一切都白搭。这个“派活”的逻辑,就是编排层最核心的价值。
JDGenie 的任务拆解经历过一次思维转变。最早版本是写死的流程模板,比如“先查价格,再查优惠,再算到手价”,看起来挺合理,但实际场景远不止一种。有的用户只关心历史价格,有的用户想对比多个购物车商品,有的用户把商品链接复制进来就想知道涨价没有。模板一多,代码里全是if...else,维护起来非常痛苦。
后来调整为理解优先、模板为辅的策略。系统先通过意图分类确定用户请求的类型,每个类型对应一组候选执行路径。单条路径上如果某个步骤失败了,系统会尝试备选路径,而不是整体报错。比如“查历史价格”的正常路径是先搜索商品定位 ID,但如果搜索接口被拦截,就切换为直接用用户提供的商品链接解析 ID。这些备选路径平时不出现在用户面前,只在关键时刻兜底。
4.2 冲突检测:当用户指令与任务状态产生矛盾时怎么办
编排层最容易被忽视、但实际最常用的功能是冲突检测。一个典型场景:用户先要求“对比这三款手机到手价”,任务还没跑完,用户又来了一句“还是先看下他们的评价吧”。如果系统老老实实去执行第二句指令,第一项任务的结果还没出来,两个任务就会打架,最后返回的东西大概率是乱的。
JDGenie 的冲突检测规则很简单但很有效:
- 如果新指令与当前任务属于同一会话且在执行中,默认进入等待队列。
- 如果新指令请求的是“查询新对象”,系统会提议终止当前任务;只有用户明确确认才中止。
- 如果新指令是“导出报告”这种独立操作,不与当前任务冲突,直接执行。
这套规则的逻辑基础是:查询类任务的结果有连续性,中途切断往往造成半成品数据;而导出、保存类任务与查询任务之间没有资源争用,可以并行。冲突检测看似是个工程问题,本质上是对“用户真实意图优先级”的判断——在拿不准的时候,宁可多问一句。
4.3 结果聚合:如何让多个工具的输出“读起来像一篇报告”
每个工具返回的都是 JSON 结构化数据,比如历史价格工具返回的是一个包含日期、价格、涨跌幅的数组。但如果直接把 JSON 堆给用户,那就不叫智能体,叫接口调试工具。JDGenie 在编排层最后一步加了一个报告生成器,负责把多个工具的输出组织成自然语言加表格的混合报告。
比如“对比这三款手机的到手价”,最终的输出结构是:
【对比结论】 三款手机中,型号B在某平台的当前到手价最低(约X元),但历史价格走势显示其近期波动较大;型号A价格最稳,当前到手价居中。建议关注型号B的降价窗口。 【明细表格】 | 型号 | 原价 | 优惠 | 到手价 | 历史最低价 | 建议 |报告生成不是简单的字符串拼接。它需要根据数据特征选择表达策略:如果某个商品的历史最低价比当前价低很多,就自动生成“降价空间较大”的结论;如果数据缺失率高,就自动加入“数据完整度有限,仅供参考”。这套逻辑本质上是把“写报告的规则”前置到代码里,用规则覆盖大多数常见情况,少数异常场景再人工判断。
从实际使用反馈看,用户对这种“给结论、给数据、给来源”的三层输出结构评价最好。先给结论照顾快阅读的人,再给明细满足想核对的人,最后附带来源让疑心重的用户有地方验证——一个结果满足三类人,这也是数据聚合设计的免费红利。
5. 部署与集成:从本地单机到服务化的演进路径
5.1 为什么先做本地单机方案
JDGenie 的部署形态最开始就确定了:本地单机优先。原因不复杂,因为系统需要访问用户的本地浏览器会话文件,这类数据放在云端会有两个麻烦。一是隐私边界会变得模糊,用户授权数据到了服务器上就说不清楚谁在看;二是调试成本高,本地环境的浏览器版本、登录状态千奇百怪,云端复现环境很难对齐用户现场。
本地单机部署的架构图非常简单:一个 Python 后端服务(负责编排和工具调用),一个本地数据库(负责缓存与日志),加上一个浏览器自动化驱动(负责执行具体操作)。三者都跑在用户自己的电脑上,没有外部依赖。这样部署的好处是,用户只要下载一个入口脚本,运行后就会自动完成环境检查、依赖安装和配置文件初始化,十分钟之内就能跑起第一轮对话。
但本地单机也有天花板:性能受机器限制,多轮大数据量任务容易卡顿;而且用户关了终端窗口,服务就停了,无法支持异步的定时任务。所以架构上从一开始就在接口层面做了兼容设计,后续想迁移到“本地客户端 + 远程任务服务”的混合模式,不需要重写业务逻辑。
5.2 迁移到服务化时保留的三个设计约束
如果 JDGenie 后续需要服务化——比如把历史价格抓取和计算这类重活放到服务端——有三个约束是我明确要求保留的:
第一,用户数据默认停留在本地。服务端只接收清洗后的商品 ID 和查询条件,不碰原始会话数据。这既是隐私设计,也是合规底线:系统能做到在没有原始数据的情况下正常服务,未来无论部署在哪,边界都很清晰。
第二,每个工具模块保持无状态。工具之间不共享运行时的内部状态,所有数据都通过上下文显式传递。这个设计在单机时代没有特别大的优势,但一旦要拆成多个服务,无状态模块天然适合水平扩展。
第三,任务执行可观测。所有任务的每个步骤都要有日志,包括入参、出参、耗时、失败原因。服务化之后,这些日志就是排查线上问题的唯一线索,如果等到迁移那一步再补日志,一定会后悔。
5.3 运行环境配置与依赖管理的经验
在本地部署的场景里,依赖管理是我踩得最深的坑之一。Python 环境的版本、浏览器驱动的版本必须和本机浏览器版本严格对应,否则会出现“代码没有任何问题,但就是跑不起来”的尴尬局面。
所以 JDGenie 的启动脚本里强制加入了环境检查逻辑,分三步:
- 检查 Python 版本是否在 3.10 到 3.12 之间,不在就直接提示用户安装正确版本,而不是让程序跑起来一半再报错。
- 检查浏览器驱动版本是否与本机浏览器主版本一致。
- 检查本地数据库目录是否有读写权限,避免任务跑到一半因为文件权限失败。
这套检查逻辑本身不复杂,但把大量“用户困惑为什么跑不起来”的问题消灭在了启动阶段。说句实在话,对于一个人维护的项目来说,减少用户提问成本就是减少自己的支持成本。系统设计不仅要考虑功能实现,还要考虑别人(甚至三个月后的自己)第一次接触时的上手成本。
6. 配置管理:用户画像、偏好设置与自动化策略的灵活组合
6.1 一套配置,三种角色
JDGenie 的配置体系分三层:全局配置、会话配置、临时覆盖。全局配置保存用户的长期偏好,比如默认关注的商品类目、默认价格预警阈值、输出报表的格式偏好。会话配置是每轮对话中间调整的参数,比如这一轮用户只想看在某个价格区间内的商品。临时覆盖则是用户在某一次请求里明确指定的特殊条件,只在当次任务中生效。
这个三级配置的设计,对应了一个很常见的实际场景:用户常年关注数码类商品(全局),某次闲聊中把目光转向家电(会话),又在某一条指令里特别指定只看某个品牌(临时)。三级参数合并的优先级很简单:临时覆盖大于会话配置大于全局配置。
合并过程有一个容易出错的地方:参数冲突。比如全局配置里价格区间是 3000 到 8000,但用户这次的临时条件是“只看 5000 以上的”。系统处理方案是“临时条件直接替换全局值”,而不是做交集。理由是临时指令反映的是当下意图,全局设置只是默认偏好,两者冲突时必须服从当下。
6.2 自动化策略配置:从手动执行到计划任务
JDGenie 的另一个实用功能是计划任务。用户可以配置“每天早上九点,检查我关注列表里商品的降价情况,如果有降价超过 5% 的,生成一份报告”。这个需求拆解下来就是三部分:定时触发器、条件过滤逻辑、报告推送方式。
定时任务在本地单机环境下有一个实现问题:电脑必须开机,且服务必须运行。JDGenie 的方案是:用操作系统的任务计划程序来唤醒服务、执行脚本、完成后退出。这样用户不需要一直开着终端,只要电脑处于开机状态,任务就能按时触发。
条件过滤逻辑是纯规则实现,比如“降价超过 X%”“价格低于 X 元”“历史价格位于近三个月低点”等。需要提醒的是,规则类的过滤器最好伴随明确的数据时间标注——系统必须在报告里标出“这条降价结论基于哪个时间点的数据”,防止用户看到过期信息产生误判。
6.3 配置文件版本迁移的兼容处理
配置文件的版本兼容是很多项目会忽略的事情。比如第一版配置文件里字段叫price_threshold,第二版改成了price_limit,如果老用户还保留着旧配置文件,升级后系统很可能因为读不到字段而静默出错。
我的处理方式是:配置文件里明确写入config_version字段,启动时先检查版本号。如果发现低版本配置,自动执行迁移脚本,把旧字段映射到新字段,并把迁移后的配置落盘。用户无感知地完成升级,老配置也不会被破坏。
这套“先在版本字段上花一个小时设计,后少花十个小时陪用户排查”的思路,适用于几乎所有带配置文件的项目。我之前在一个内部工具上没做配置版本管理,结果升级后三分之一的用户遇到异常,排查了很久才发现是旧配置文件字段没对上。从那以后,我对配置迁移有了执念。
7. 实战中的问题排查:定位任务失败的关键链路
7.1 一次典型的“任务卡在查询环节”排查过程
在真实使用中遇到的一个非常典型的问题是:用户发起“查历史价格”任务,系统一直停留在“查询商品信息”这一步,没有任何后续输出。这个问题的排查过程对理解 JDGenie 的架构很有帮助。
我首先查看的是任务日志中的入参和出参。入参里有商品链接,说明用户输入的实体识别成功了;出参为空,说明工具模块内部可能抛了异常。再往下查,发现工具模块调用了一个商品搜索接口,这个接口返回的状态码是 200,但响应体里没有商品 ID 字段——页面结构在无人通知的情况下发生了改版。
问题找到后,修复动作分三步:第一,临时把该商品的 ID 解析逻辑切换为备用方案(直接用 URL 中的数字标识);第二,更新标准选择器;第三,把这次改版导致的失败记录加进“页面变化检测”样本库。整套排查过程中,最关键的其实是任务日志的完整度——如果当时没有按步骤记录入参出参,这个问题可能得花一个下午才能定位。
7.2 数据准确性问题的常见源头与规避手段
另一个常见问题是:用户反馈“系统推荐的到手价比我实际下单看到的贵”。排查后发现,原因是到手价的算法没有把某些平台的补贴算进去。这类问题比较隐蔽,因为数据格式是对的结构也挖对了,只是少算了一个变量。
JDGenie 的规避手段有两个。一是计算结果必须附带“计算口径说明”,例如“本价格已包含促销直降与优惠券,未包含平台补贴与支付优惠”,让用户明白数据覆盖了哪些因素。二是价格数据保留原始页面快照的引用链接,用户一看出入可以点击跳转原页面核对。
这两个手段都算不上高科技,但非常实用——数据系统的公信力不是靠准确率 100% 建立的,而是靠“敢透明、可核对”建立的。标注已覆盖和未覆盖的因素,比追求一个大而全但说不清口径的“最终价”更能获得用户信任。
7.3 自动化工具被拦截时的优雅降级
数据采集被拦截几乎是所有自动化系统都会遇到的问题。JDGenie 的应对策略不是“对抗”,而是“降级”。一旦系统检测到连续多次请求返回异常特征,自动进入慢速模式:延长请求间隔、开启随机延迟、降低单批次抓取数量。
同时,系统会在前端提示“当前网络环境限制较多,自动抓取速度已调慢,预计需要更长时间完成”。这种透明提示比默默变慢要好得多——用户至少知道系统还在跑,而不是以为卡死了。
降级策略的核心逻辑是将“抓取能力”视为一个有预算的资源,而不是无限供应的水龙头。这个思路在所有依赖外部网页数据的系统设计里都通用——外部接口对调用方从来都不是友好的,只有把自己的行为规范好,才能长期稳定运行。
8. 复盘总结与后续演进方向
JDGenie 从最初的脚本集合,到现在的智能体编排系统架构,中间经历了几次比较大的重构。每次重构的起点都不是“觉得代码丑了”,而是某一个具体的问题憋不住了:状态混乱、模块耦合、数据不透明、页面变化难应对。架构设计在这里的角色不是追时髦,而是给这些问题找一套相对稳定的解题框架。
如果让我提炼这套架构里最值得带走的三条经验,我会选:
- 模块化工具链加统一执行接口,让新能力的接入成本降到最低。
- 任务状态与会话状态分离,避免长任务阻塞整体交互。
- 数据结果总是附带计算口径和来源标注,让系统在数据不全时依然可解释、可信赖。
后续演进方向上,JDGenie 有几个可以扩展的点。一是引入更灵活的规则引擎,把“条件过滤、冲突检测、报告策略”等逻辑从硬编码中解放出来,让用户通过简单配置就可以自定义系统的行为偏好。二是增强上下文记忆能力,当前的状态机已经能处理几轮连续对话,但跨天、跨周的用户偏好记忆还需要更多沉淀策略——这部分需要仔细权衡,记忆越多,隐私边界的压力越大。三是考虑把任务执行拆成可独立运行的服务,为将来的云端协作或多人共享任务队列做准备。
这些方向短时间内都不会全部落地,但架构上留有足够的接口空间。对我个人来说,这个项目的最大收获是验证了一件事:一个人维护的中型智能体系统,只要把模块边界切清楚、把数据行为管明白、把失败链路设计好,是完全可以做到“既有工程严谨度、又保持小而灵活”的。这套架构文档如果能给正在做同类项目的朋友一些启发,就不白写。