数字可验证
ReAct Agent 通过 CODE 动作在真实 DataFrame 上执行 Python,OBSERVATION 回传计算结果,DONE 报告引用真实数字,杜绝 LLM 幻觉。
FinQuery · 企业智能数据分析平台
基于 LangChain 多 Agent 设计范式构建的企业级 Excel 智能分析系统。上传表格、自然语言提问,Multi-Agent 协作完成读表、计算、可视化与报告生成。
ReAct Agent 通过 CODE 动作在真实 DataFrame 上执行 Python,OBSERVATION 回传计算结果,DONE 报告引用真实数字,杜绝 LLM 幻觉。
采用 Orchestrator 编排 + Tool 工具链 + Memory 记忆 + Context 上下文传递,完整实现 LangChain Multi-Agent 架构模式。
COM-First 读取策略,自动处理合并单元格、多 Sheet、公式与中文编码,生成 Rich Schema 注入 Agent Prompt。
内置 6 种 Plotly 图表(折线/柱状/饼图/散点/热力/瀑布),关键词自动触发;Agent 亦可用 matplotlib 自由绑图,图表嵌入聊天气泡。
系统完整实现了 LangChain 生态中的 Multi-Agent 设计模式:Supervisor 编排、Tool Use、Memory、State 传递与 ReAct 推理-行动循环。各模块职责清晰、可独立扩展。
┌─────────────────────────────────────────────────────────────────────┐
│ Supervisor Agent · Orchestrator │
│ 协调 Context / Memory / AnalysisEngine / LLM,驱动 ReAct 主循环 │
└───────────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Context Agent │ │ Memory Agent │ │ Analysis Agent │
│ ContextManager│ │ MemoryManager │ │ AnalysisEngine │
│ 多文件/多Sheet │ │ 项目记忆+自动记忆│ │ 工具路由+代码执行 │
│ DataFrame状态 │ │ 关键词相关性召回 │ │ 沙箱 Python REPL │
└───────┬───────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────────┼───────────────────────┘
▼
┌─────────────────────┐
│ LLM Agent Gateway │
│ 双 Provider 热切换 │
│ internal / external │
└─────────────────────┘
| LangChain 概念 | 本系统实现 | 源码位置 |
|---|---|---|
| Agent / Supervisor | Orchestrator — 主编排器,驱动 ReAct 循环,协调所有子模块 | agents/orchestrator.py |
| BaseAgent 接口 | BaseAgent 抽象类,定义 name / description / system_prompt / tools / process | agents/base_agent.py |
| Tool | BaseTool 及 DataProfiler / Statistics / Chart / PythonExecutor | tools/ |
| Agent Executor | handle_message() — 最多 8 轮 ReAct 循环,解析 ACTION 并分发 | agents/orchestrator.py |
| Memory | MemoryManager — 项目记忆(长期)+ 自动记忆(相关性召回) | memory/memory_manager.py |
| State / Context | ContextManager — 跨 Agent 传递 DataFrame、Schema、元数据 | context/context_manager.py |
| LLM Chain | call_llm() — System Prompt + Messages → ChatCompletion | agents/llm_client.py |
| Callbacks | progress_cb — 每轮推送 step / observation 事件到 SSE 流 | web/routes.py |
| Tool Router | AnalysisEngine.run() — 关键词路由到内置工具,预计算注入 Prompt | tools/analysis_engine.py |
系统核心编排器,等价于 LangChain 中的 Supervisor / Agent Executor。接收用户消息后:
AnalysisEngine 预执行关键词匹配的内置工具build_rich_schema() 构建完整数据 Schema 注入 System PromptMemoryManager 召回 top-5 相关历史记忆管理当前分析会话的数据状态,在 Agent 间传递上下文(类似 LangChain Runnable State):
_tables — 多文件 × 多 Sheet 的 DataFrame 字典_metadata — 文件存储路径、行数列数等元信息_merge_maps — 合并单元格展开记录get_active_df() — 当前活跃 Sheet 的 DataFrame(注入为 df 变量)双层记忆架构,对标 LangChain ConversationBufferMemory + 相关性检索:
memory_entries 表,支持 tags 标签匹配运行时可在两套 LLM Provider 间热切换,无需重启服务:
| internal | 公司内部模型(Bodor AI · Qwen3-235B),内网直连,适合敏感数据 |
|---|---|
| external | OpenRouter 外部模型(xiaomi/mimo-v2.5-pro),适合复杂推理 |
支持 3 次指数退避重试、请求/响应 JSONL 全链路日志、OpenRouter 专属 HTTP-Referer 头。
所有工具继承 BaseTool 抽象类(对标 LangChain BaseTool),由 AnalysisEngine 按关键词路由自动调用。
| 工具 | 触发关键词 | 能力 |
|---|---|---|
DataProfilerTool | 质量、缺失、异常、重复、诊断 | 缺失值统计、异常值检测、重复行分析、数据概览 |
StatisticsTool | 同比、环比、趋势、相关、结构、占比 | YoY/MoM 计算、趋势分析、相关性矩阵、结构分布 |
ChartTool | 图表、柱状、饼图、折线、可视化 | Plotly 交互式图表,输出 HTML 嵌入报告 |
Python REPL | 计算、汇总、groupby、pivot、排名 | 沙箱执行 Agent 生成的 Python 代码,暴露 df / pd / np |
ExcelReader | (上传时自动) | COM → openpyxl → xlrd 三级回退,合并单元格自动展开 |
路由优先级:内置工具(快、结构化)→ Python REPL(灵活、复杂逻辑)→ LLM 叙述(解读结果)。
系统内置完整的图表生成能力,支持 双通道出图:内置 ChartTool 自动绘图 + Agent 代码中 matplotlib 自定义绘图。图表以 Plotly 交互式 HTML 嵌入聊天气泡,支持缩放、悬停、图例切换。
当用户消息包含图表关键词(图表、柱状、饼图、折线、可视化、画图、散点等),AnalysisEngine 在 Agent 循环启动前自动调用 ChartTool,根据语义智能选择图表类型:
趋势 / 时间序列分析。触发词:趋势、折线、trend
对比 / 排名分析。支持垂直/水平、多组并列、Top-N 筛选
占比 / 结构分析。触发词:饼、占比、结构。自动限制 ≤8 类
相关性 / 分布分析。大数据集自动采样至 5000 点
多变量相关矩阵。触发词:热力、相关矩阵
增减分解 / 利润桥分析。触发词:瀑布、增减
| 自动轴检测 | 根据列类型和唯一值数量,自动选择 X 轴和颜色分组维度 |
|---|---|
| 模糊列名匹配 | 支持大小写不敏感、子串匹配,容错用户口语化列名 |
| 内置聚合 | group_by + agg_func(sum / mean / count / max / min)先聚合再绘图 |
| 多组对比 | color 维度支持最多 12 组,超出自动合并为 Other |
| Top-N 筛选 | 柱状图/饼图自动取 Top 15,饼图超过 8 类自动截断 |
| 交互式输出 | Plotly HTML 嵌入聊天气泡,前端通过 Plotly.js 渲染,支持悬停/缩放 |
ReAct 循环中 Agent 可在 CODE 动作里使用 matplotlib 自由绘图(已预配置中文字体)。适合复杂多子图、自定义样式等 ChartTool 无法覆盖的场景。代码中的 plt.show() 输出会被捕获并展示。
用户提问「画一个区域预算占比饼图」
│
▼
AnalysisEngine 关键词命中 _CHART_KW
│ 自动选择 chart_type = "pie"
│ 检测 group_by 列(从消息中匹配列名)
▼
ChartTool.execute(df, chart_type="pie", group_by="区域", top_n=8)
│ Plotly 生成交互式 HTML
▼
charts[] 随 Agent 结果返回
│
├─ SSE done 事件 → chat.js appendMessage(reply, charts)
├─ 聊天气泡内嵌 Plotly 图表(.chart-inline)
└─ 会话历史 metadata.charts 持久化,刷新后可回放
| 「各区域销售额柱状图」 | 自动 bar 图 + 按区域 group_by 聚合 |
|---|---|
| 「月度收入趋势折线图」 | 自动 line 图 + 时间轴 |
| 「各部门预算占比饼图」 | 自动 pie 图 + Top 8 部门 |
| 「画一个价格与销量的散点图」 | 自动 scatter 图 + 采样 |
| 「用代码画分组箱线图」 | Agent CODE → matplotlib 自定义绑图 |
对标 LangChain ReAct Agent:每轮 LLM 输出一个 ACTION,系统执行后返回 OBSERVATION,循环直至 DONE 或达到上限。
编排器支持多种 LLM 输出格式,确保不同模型的 Tool Call 均可正确解析:
ACTION: THINK|CODE|DONE<CODE>...</CODE> 自动转换为标准 Python 块```python 自动识别为 CODE| 可用变量 | df(活跃 Sheet)、df_<sheet名>(各 Sheet)、pd、np |
|---|---|
| 输出捕获 | stdout 重定向,DataFrame 自动转 Markdown 表格 |
| 中文字体 | matplotlib 预配置 CJK 字体(Noto Sans CJK / 文泉驿等) |
| 安全限制 | 无文件系统 / 网络访问,代码在隔离 namespace 中 exec |
| 日志 | 每次执行记录至 data/code_logs/ JSONL |
用户浏览器
│ POST /api/chat/stream { message, session_id, file_ids }
▼
FastAPI 路由层
│ ① 创建/验证 Session,保存用户消息到 SQLite
│ ② 生成 task_id,注册后台任务 _TASKS[task_id]
│ ③ 立即返回 { task_id, session_id }
▼
后台 Agent 任务 (_run_background_agent)
│ ④ 并行加载 Excel → ContextManager.add_file()
│ ⑤ 加载 MemoryManager + 聊天历史(最近 6 条,去重截断)
│ ⑥ Orchestrator.handle_message() → ReAct 循环
│ ├─ AnalysisEngine 预计算工具结果
│ ├─ call_llm() × N 轮
│ ├─ execute_code_from_reply() 沙箱执行
│ └─ progress_cb → SSE 事件推送
│ ⑦ 保存 Assistant 消息 + metadata(agent_steps, charts)
│ ⑧ log_conversation() → data/chat_logs/ JSONL
▼
SSE 事件流 GET /api/task/{task_id}/events
│ progress → step → observation → done / error
▼
前端 chat.js 实时渲染 → Markdown 报告 + Plotly 图表
| type | 说明 | 关键字段 |
|---|---|---|
progress | 阶段进度(加载文件、历史、启动 Agent) | step, text |
step | Agent 动作(THINK / CODE / DONE) | action, turn, text |
observation | 代码执行结果 | turn, text |
done | 分析完成 | reply, charts[], session_id |
error | 分析失败 | text |
df_<safe_name> 用户浏览器
│ HTTPS
▼
finquery.lijichen.cn :443
│
Xray SNI 路由(stream proxy)
│
▼
Nginx :4443 (SSL Termination)
│ proxy_pass
▼
fenxi.service (systemd)
Uvicorn → FastAPI :7860
│
┌─────────────┼─────────────┐
▼ ▼ ▼
SQLite DB 用户文件目录 JSONL 日志
excel_analyst.db data/users/ llm_logs/
code_logs/
chat_logs/
| 进程管理 | systemd fenxi.service,异常自动重启(RestartSec=3) |
|---|---|
| SSL 证书 | Let's Encrypt,通过 ACME Webroot 自动续期 |
| 文件限制 | 单文件最大 50MB,支持 .xlsx / .xls / .csv |
| 上传限制 | Nginx client_max_body_size 512MB |
| 日志类型 | 路径 | 记录内容 |
|---|---|---|
| LLM 调用日志 | data/llm_logs/llm_log_YYYY-MM-DD.jsonl | request_id, model, system_prompt, messages, response, duration, status |
| 代码执行日志 | data/code_logs/code_exec_YYYY-MM-DD.jsonl | 完整 Python 代码、stdout 输出、成功/失败、耗时 |
| 对话日志 | data/chat_logs/chat_YYYY-MM-DD.jsonl | 用户问题、agent_steps、code_executions、最终回复、总轮次 |
| 服务日志 | journalctl -u fenxi | 启动、LLM 请求、错误堆栈、任务状态 |
JWT Token(HttpOnly Cookie),bcrypt 密码哈希,24 小时过期。所有 API 通过 get_current_user 依赖注入鉴权。
用户级文件目录 data/users/{id}/,数据库查询均带 user_id 过滤,会话/文件/记忆严格归属校验。
Agent 生成的 Python 在受限 namespace 中执行,无网络/文件系统权限,stdout 截断至 4000 字符防溢出。
| Web 框架 | FastAPI 0.115 + Uvicorn(全异步 ASGI) |
|---|---|
| ORM | SQLAlchemy 2.0 + aiosqlite(异步 SQLite) |
| Agent 架构 | LangChain 风格 Multi-Agent(Orchestrator + Tools + Memory + ReAct) |
| LLM 接入 | httpx 异步 HTTP · OpenAI 兼容 ChatCompletion API |
| 数据分析 | Pandas 2.2 · NumPy 2.2 · SciPy 1.15 · Plotly 5.24 |
| Excel 引擎 | win32com (COM) → openpyxl → xlrd → pandas 四级回退 |
| 认证 | python-jose (JWT) + passlib (bcrypt) |
| 前端 | 原生 HTML/CSS/JS · Marked.js (Markdown) · MathJax · Plotly.js |
| 部署 | systemd + Nginx + Xray SNI + Let's Encrypt |
所有 API 均需登录认证(Cookie: access_token),管理员接口额外校验 is_admin。Base URL: https://finquery.lijichen.cn
/api/login用户登录,返回 JWT Cookie。
// Request
{ "username": "admin", "password": "admin123" }
// Response 200
{ "ok": true, "username": "admin", "display_name": "管理员" }
/api/logout退出登录,清除 Cookie。
// Response 200
{ "ok": true }
/health服务健康检查(无需认证)。
// Response 200
{ "status": "ok", "version": "1.0.0" }
/api/settings/provider获取当前 LLM 提供方。
// Response 200
{ "provider": "internal", "label": "公司内部模型", "model": "Qwen3-235B-A22B" }
/api/settings/provider切换 LLM 提供方(运行时热切换)。
// Request
{ "provider": "external" } // "internal" | "external"
// Response 200
{ "provider": "external", "label": "外部模型", "model": "xiaomi/mimo-v2.5-pro" }
/api/files列出当前用户的所有活跃文件。
// Response 200
[
{
"id": 1, "name": "预算表.xlsx", "size": 102400,
"type": "xlsx", "sheets": ["Sheet1", "汇总"],
"rows": 500, "cols": 12, "uploaded_at": "2026-06-17T01:00:00"
}
]
/api/files/upload上传 Excel/CSV 文件(multipart/form-data,字段名 file)。
// Response 200
{
"id": 1, "name": "预算表.xlsx", "sheets": ["Sheet1"],
"rows": 500, "cols": 12, "reader": "openpyxl", "merge_count": 3
}
/api/files/{file_id}软删除文件(标记 is_active=false)。
// Response 200
{ "ok": true }
/api/sessions列出用户所有活跃会话。
// Response 200
[{ "id": 1, "title": "区域预算分析", "created_at": "...", "updated_at": "..." }]
/api/sessions创建新会话。
// Request
{ "title": "新对话" }
// Response 200
{ "id": 2, "title": "新对话" }
/api/sessions/{session_id}软删除会话。
/api/sessions/{session_id}/messages获取会话全部消息(含 agent_steps、charts 等 metadata)。
// Response 200
[
{
"id": 1, "role": "user", "content": "各区域预算占比?",
"metadata": {}, "created_at": "..."
},
{
"id": 2, "role": "assistant", "content": "## 分析报告\n...",
"metadata": {
"agent_steps": [{"action": "CODE", "turn": 1, "code": "...", "observation": "..."}],
"agent_turns": 3,
"charts": [{"type": "pie", "title": "区域占比", "html": "..."}]
},
"created_at": "..."
}
]
/api/chat/stream推荐 — 异步流式分析。立即返回 task_id,通过 SSE 接收实时进度。
// Request
{
"message": "按区域汇总预算并计算占比",
"session_id": 1, // 可选,不传则自动创建
"file_ids": [1, 2] // 要分析的文件 ID 列表
}
// Response 200(立即返回)
{ "task_id": "uuid-...", "session_id": 1 }
// 然后连接 SSE
GET /api/task/{task_id}/events?after=0
// Content-Type: text/event-stream
data: {"type":"progress","step":"load_files","text":"文件已就绪:预算表.xlsx"}
data: {"type":"step","action":"CODE","turn":1,"text":"执行代码中…"}
data: {"type":"observation","turn":1,"text":"区域 | 预算\n华东 | 1200\n..."}
data: {"type":"step","action":"DONE","turn":2,"text":"生成报告"}
data: {"type":"done","reply":"## 分析报告\n...","charts":[...],"session_id":1}
/api/chat同步分析(阻塞等待,适合简单调用或脚本集成)。
// Request — 同 /api/chat/stream
// Response 200
{
"session_id": 1,
"reply": "## 分析报告\n...",
"charts": [{ "type": "pie", "title": "区域占比", "html": "<div>..." }]
}
/api/task/{task_id}/eventsSSE 事件流。参数 after 为已接收事件索引,支持断线重连回放。
/api/task/{task_id}/resume页面刷新后恢复任务。运行中返回 status=running,已完成返回完整 result。
// Response 200
{ "task_id": "...", "status": "done", "session_id": 1, "result": { "type": "done", "reply": "...", "charts": [] } }
/api/task/{task_id}/status轻量轮询端点,检查任务状态和事件数量。
// Response 200
{ "task_id": "...", "status": "running", "session_id": 1, "event_count": 5, "result": null }
/api/admin/users列出所有用户及在线状态。
/api/admin/users创建新用户。
// Request
{ "username": "user1", "password": "pass123", "display_name": "张三" }
// Response 200
{ "id": 2, "username": "user1" }
/api/admin/users/{user_id}/toggle启用/禁用用户账号。
// Response 200
{ "id": 2, "is_active": false }
| 401 | 未登录或 Token 过期 |
|---|---|
| 403 | 无管理员权限 / 账号已禁用 |
| 404 | 会话 / 文件 / 任务不存在 |
| 400 | 参数错误(空消息、文件格式不支持、文件过大等) |