某一天,你的老板提了一个在线复杂表单填写信息录入的需求,于是你采用了主流的做法:FastAPI + React 编写了前后端程序。当开发完后,老板忽然给你的团队提了一个需求:"小程啊,这个表单一个一个填写太繁琐了,客户那边有时候想要直接传一个模板文件需要解析这里的数据来填写,你看看能不能借助 AI 来实现这个功能"。你回了一句:"这个表单本身填写就很复杂,不然为什么要单独写一个页面来填写呢?",话虽如此,你还是研究了下。
这时候你脑海里有两个方案:
方案 1:你需要查看前端代码,分析在各种分支条件下构建的数据结构,再在后端补 REST API。当你看到这个表单有多个问题涉及分支渲染后,你想偷懒,选择第二个方案。
方案 2:因为分支条件多、不同选项渲染的字段不一致,你用 Browser Agent(截图 + 点 DOM)演示了怎么填表,也教了客户正确姿势。
好,问题解决,全剧终——开个玩笑。
几天后,老板又找你:"小程啊,龙虾填表是不错,但 token 消耗太大"。确实,Browser Agent 要把截图+ vision或dom 塞进上下文,而且操作起来巨慢无比。你眼看就要打开 Cursor,准备为ai的调用编写代码。
这时候你想起,前阵子看的Reflex 团队发布了新特性:用 Reflex 写的页面,每个交互事件可以自动生成 HTTP API(参见 agent-benchmark),决定试试用这个方案来对比下。
Reflex 是什么(一句话)
Reflex 是 Python 全栈框架:页面用 Python 写,编译成 React;状态用 State 类管理;前后端通信用 Starlette(FastAPI 的底层)。启用 Reflex Enterprise 的 EventHandlerAPIPlugin 后,你写 UI 事件 handler 的同时,就是在写 AI 能调的 HTTP API(无需另建 REST 层)。
下面用一个简化版条件表单说明。更多例子建议阅读官方博客:https://reflex.dev/blog/vision-agents-vs-api-calls/
前端代码示例:
核心就三件事:Select 改分支变量 → rx.match 换一整块 UI → rx.cond 处理嵌套分支。
import reflex as rxfrom myform.state import Statedef personal_fields(): # 某个分支的组件集合 return rx.hstack(...)def enterprise_fields(): # 同理...def identity_section(): return rx.vstack( # 1. Select 绑定 State,选中值变化时触发 event rx.select( ["个人", "企业", "政府"], value=State.identity_type, on_change=State.set_identity_type, ), # 2. 根据 identity_type 渲染完全不同的字段组 rx.match( State.identity_type, ("个人", personal_fields()), # 姓名、身份证、邮箱 ("企业", enterprise_fields()), # 公司名、税号、联系人 ("政府", government_fields()), # 部门、公函号、经办人 ), ) def refund_fields(): return rx.vstack( rx.input(value=State.answers["order_id"], on_change=...), rx.select( ["质量问题", "发错货", "商品损坏"], value=State.refund_reason, on_change=State.set_refund_reason, ), # 3. 嵌套条件:只有「发错货」才出现换货偏好 rx.cond( State.refund_reason == "发错货", rx.select(["同款换货", "直接退款"], ...), ), )
读者有 Python 基础的话,可以把 rx.vstack / rx.select 理解成「声明式 UI 组件树」;State.xxx 是响应式绑定,值一变页面自动重绘。
后端 State:事件即逻辑
State 里放少量分支变量 + 一个 answers 字典,复杂结构提交时用 Pydantic 组装
import reflex as rxfrom pydantic import BaseModelclass State(rx.State): identity_type: str = "个人" request_type: str = "技术支持" refund_reason: str = "质量问题" answers: dict[str, str] = {"name": "", "company_name": "", ...} @rx.var def visible_field_keys(self) -> list[str]: """告诉 AI:当前分支下应该填哪些 key(不用猜 DOM)。 当self.identity_type或者self.request_type变化后, 这个状态变量会自动更新 """ keys = IDENTITY_FIELDS[self.identity_type] keys += REQUEST_FIELDS[self.request_type] if self.request_type == "退款" \ and self.refund_reason == "发错货": keys.append("exchange_preference") return keys @rx.event def set_identity_type(self, value: str): """切换身份分支。docstring 可直接给 AI 当 tool 说明。""" self.identity_type = value @rx.event def set_answer(self, key: str, value: str): self.answers = {**self.answers, key: value} @rx.event def submit_form(self): form = ApplicationForm( # Pydantic discriminated union identity=build_identity(self), request=build_request(self), ) self.result = form.model_dump_json()
注意:@rx.event 包装的事件函数,正常网页用户点按钮会调用,对于AI Agent 也可以 HTTP POST 调同一个方法——这是和 FastAPI + React 方案最大的区别,像比如表单的场景下的常用做法,一般状态变量都是由前端去设置,当后续填完后一次性发给后端
对比 FastAPI + React,Reflex 做了什么
外部系统或 Agent 要调用「和人一样的功能」,常见有三条路(官方博客 也做了 benchmark):
| | |
|---|
| | 嵌套 rx.cond 字段未挂载时易漏填,token 贵、慢 |
| 调 REST,常需先 GET schema 再 PATCH | 要手写 schema、校验、与前端 visibility 对齐,维护第二套逻辑 |
| Reflex Event API | POST 调 @rx.event 暴露的 endpoint | 切换分支 = 调 set_identity_type 等;读响应里的 visible_field_keys |
「无状态 API」和 Reflex「有状态会话」的区别
当你为 Agent 单独编写的那套 REST/MCP 接口,往往按 REST 惯例设计——每次请求自带完整参数(customer_id、order_id、表单字段……),服务端不会自动记住「上一步选了哪个分支、草稿填到哪了」。
多步表单就要么:
- 你自己再实现 session / 草稿存储 /
GET /form/schema 这类「当前该填什么」的接口
Reflex Event API 走的是另一条路:Agent 和用户共用同一个 State 类。
- 每次
POST .../set_identity_type 会改 State.identity_type,下次 POST .../set_answer自动继承已选分支和已填的 answers - 效果上像人在页面上连续操作,不必再维护一套与 UI 条件渲染对齐的 API 状态机
- 会话靠
Authorization: Bearer <uuid> 绑定(OpenAPI 里 401 即未带 token);同一 token 下的多次调用共享一份 State
所以更准确的说法是:不是 HTTP magically 有状态,而是 Reflex 把 UI 的状态层直接暴露给了 Agent,省掉了「为自动化再建一套会话模型」的工程。
Event API 长什么样
在 rxconfig.py 启用 EventHandlerAPIPlugin(需 Reflex Enterprise)后,OpenAPI 文档位于:
http://localhost:8000/_reflex/events/openapi.yaml
本仓库表单对应的 handler 路径示例(路径由 app 名 + 模块 + State 类名生成):
POST /_reflex/event/app___state_____state/set_identity_type {"value": "企业"}POST /_reflex/event/app___state_____state/set_answer {"key": "company_name", "value": "杭州示例科技"}POST /_reflex/event/app___state_____state/submit_form {}
OpenAPI 片段(set_request_type):
/_reflex/event/.../app_state/set_request_type:post:summary:"设置问题2「业务类型」,切换 rx.match 渲染的业务字段组。"description:| value: 「技术支持」|「退款」|「定制开发」。 副作用:清除 submit 结果;若选退款,还需配合 set_refund_reason 处理嵌套分支。operationId:State_set_request_typerequestBody:content:application/json:schema:properties:value: { type:string }required: [value]responses:'200':$ref:'#/components/responses/StreamedDelta'# NDJSON 状态增量'401':$ref:'#/components/responses/Unauthorized'tags: [State]
给 @rx.event 方法写好 docstring,description / summary 会自动进 OpenAPI,可直接当 LLM 的 tool 说明用。
AI Agent 填表流程(Structured-API)
- 生成会话 token,后续请求带上
Authorization: Bearer <uuid> - (可选)
POST .../reset_form 清空表单
POST set_identity_type {"value": "企业"}POST set_answer {"key": "company_name", "value": "..."} × NPOST set_request_type {"value": "退款"}POST set_refund_reason {"value": "发错货"} ← 嵌套字段出现POST set_answer {"key": "exchange_preference", "value": "同款换货"}POST submit_form {}
响应是 NDJSON 流(StreamedDelta),Agent 解析每行 state delta 即可读到更新后的 visible_field_keys、submit_message 等——不用解析 HTML,也不用先 GET 一份动态 schema。
小结
这个特性出来后,reflex开发的app,个人感觉更是在ai agent时代下需要的app,因为未来app的使用者不一定只有真人,很多任务其实可能会由agent完成,比如让ai帮我找出某个客户,让ai帮我更新某个订单的状态,让ai帮我把某个工作流做完
这时候我们的app的功能就不只是设计给人类在UI组件上点击,它也需要能够被agent或者其他自动化程序进行调用
Reflex Enterprise 的 EventHandlerAPIPlugin 的意义就在这里。
如果我们用传统的webapp开发的流程,先是做UI给人使用。而当我们需要自动化的时候,就会补上api。之后需要ai agent调用的时候,又要另外用langchain之类的设计代码。久而久之UI代码,api代码,agent调用代码就会各自分散,维护成本会越来越高。
但 Reflex 的思路在于:人走 UI,Agent 走同一套 event handler;会话状态在 State 里连续积累,不必再为自动化单独设计 REST 会话层。Agent 侧主要工作变成 tool 编排(调用顺序、解析 NDJSON 响应),复杂条件分支下的「该填哪些字段」交给 visible_field_keys 和 handler docstring。
参考链接:
https://reflex.dev/blog/vision-agents-vs-api-calls/
https://github.com/reflex-dev/agent-benchmark