perf: switch to single-stage routing and low-latency defaults
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
2026-03-03 14:36:33 +08:00
parent c23b1bb203
commit 77391daf77
4 changed files with 70 additions and 18 deletions

43
qingjian_cs/.env.example Normal file
View File

@@ -0,0 +1,43 @@
QINGJIAN_WS_URI=ws://127.0.0.1:9528
OPENAI_API_KEY=
OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
OPENAI_MODEL_NAME=doubao-seed-2-0-pro-260215
# 低延迟参数(首答快)
MESSAGE_DEBOUNCE_SECONDS=0.2
IMAGE_MESSAGE_DEBOUNCE_SECONDS=0.4
AUTO_QUOTE_WAIT_SECONDS=18
AGENT_MAX_ITERS=1
DECISION_TIMEOUT_SECONDS=8
STORE_BACKEND=mysql
STORE_SQLITE_PATH=
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_DATABASE=ai_cs
MYSQL_TABLE_PREFIX=qjcs_
HTTP_HOST=127.0.0.1
HTTP_PORT=6060
TIANWANG_CALLBACK_URL=http://139.199.3.75:18789/api/callback
# 企业微信通知(留空即关闭)
WECHAT_WEBHOOK=
# 邮件通知(留空即关闭)
EMAIL_NOTIFY_ENABLED=0
SMTP_HOST=smtp.qq.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_TO=
# 自动作图
AUTO_DRAW_ENABLED=1
AUTO_DRAW_ENDPOINT=
AUTO_DRAW_TIMEOUT_SECONDS=25
# 并发
MAX_CONCURRENT_TURNS=4

View File

@@ -6,12 +6,11 @@
- `OpenAIChatFormatter` - `OpenAIChatFormatter`
- `InMemoryMemory` - `InMemoryMemory`
- `structured_model` 输出 - `structured_model` 输出
- `Orchestrator` 编排(Router + Specialists - `Orchestrator` 编排(单阶段直达 Specialists
## 架构 ## 架构
- `app/agents.py` - `app/agents.py`
- `RouterAgent`
- `PreSalesAgent` - `PreSalesAgent`
- `QuoteAgent` - `QuoteAgent`
- `AfterSalesAgent` - `AfterSalesAgent`
@@ -32,11 +31,20 @@
- `OPENAI_BASE_URL` 默认 `https://ark.cn-beijing.volces.com/api/v3` - `OPENAI_BASE_URL` 默认 `https://ark.cn-beijing.volces.com/api/v3`
- `OPENAI_MODEL_NAME` 默认 `doubao-seed-2-0-pro-260215` - `OPENAI_MODEL_NAME` 默认 `doubao-seed-2-0-pro-260215`
- `AUTO_QUOTE_WAIT_SECONDS` 默认 `18` - `AUTO_QUOTE_WAIT_SECONDS` 默认 `18`
- `MESSAGE_DEBOUNCE_SECONDS` 默认 `6` - `MESSAGE_DEBOUNCE_SECONDS` 默认 `0.2`
- `IMAGE_MESSAGE_DEBOUNCE_SECONDS` 默认 `0.4`
- `AGENT_MAX_ITERS` 默认 `1`
- `DECISION_TIMEOUT_SECONDS` 默认 `8`
- `STORE_BACKEND` 默认 `sqlite`,可设 `mysql` - `STORE_BACKEND` 默认 `sqlite`,可设 `mysql`
- `STORE_SQLITE_PATH` 可选 - `STORE_SQLITE_PATH` 可选
- `MYSQL_HOST/MYSQL_PORT/MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE` - `MYSQL_HOST/MYSQL_PORT/MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE`
可直接复制模板:
```powershell
copy .env.example .env
```
## 启动uv run ## 启动uv run
当前环境建议使用(避免上层工程依赖干扰): 当前环境建议使用(避免上层工程依赖干扰):
@@ -45,6 +53,15 @@
uv run --with websockets --with pydantic --with openai --with pymysql python run.py uv run --with websockets --with pydantic --with openai --with pymysql python run.py
``` ```
### 生产建议(天网)
- WebSocket单实例防重复回复
- HTTP API多 worker提升并发
```powershell
uv run powershell -File .\scripts\start_tianwang_prod.ps1 -Host 127.0.0.1 -Port 6060 -ApiWorkers 2
```
## Golden 回放 ## Golden 回放
```powershell ```powershell

View File

@@ -12,8 +12,8 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3").strip() OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3").strip()
OPENAI_MODEL_NAME = os.getenv("OPENAI_MODEL_NAME", "doubao-seed-2-0-pro-260215").strip() OPENAI_MODEL_NAME = os.getenv("OPENAI_MODEL_NAME", "doubao-seed-2-0-pro-260215").strip()
MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "0.8")) MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "0.2"))
IMAGE_MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("IMAGE_MESSAGE_DEBOUNCE_SECONDS", "1.2")) IMAGE_MESSAGE_DEBOUNCE_SECONDS = float(os.getenv("IMAGE_MESSAGE_DEBOUNCE_SECONDS", "0.4"))
AUTO_QUOTE_WAIT_SECONDS = int(os.getenv("AUTO_QUOTE_WAIT_SECONDS", "18")) AUTO_QUOTE_WAIT_SECONDS = int(os.getenv("AUTO_QUOTE_WAIT_SECONDS", "18"))
AGENT_MAX_ITERS = int(os.getenv("AGENT_MAX_ITERS", "1")) AGENT_MAX_ITERS = int(os.getenv("AGENT_MAX_ITERS", "1"))
FAST_ROUTE_ENABLED = os.getenv("FAST_ROUTE_ENABLED", "1").strip() in {"1", "true", "True", "yes", "on"} FAST_ROUTE_ENABLED = os.getenv("FAST_ROUTE_ENABLED", "1").strip() in {"1", "true", "True", "yes", "on"}
@@ -47,4 +47,4 @@ AUTO_DRAW_TIMEOUT_SECONDS = int(os.getenv("AUTO_DRAW_TIMEOUT_SECONDS", "25"))
# 并发与超时 # 并发与超时
MAX_CONCURRENT_TURNS = int(os.getenv("MAX_CONCURRENT_TURNS", "4")) MAX_CONCURRENT_TURNS = int(os.getenv("MAX_CONCURRENT_TURNS", "4"))
DECISION_TIMEOUT_SECONDS = int(os.getenv("DECISION_TIMEOUT_SECONDS", "60")) DECISION_TIMEOUT_SECONDS = int(os.getenv("DECISION_TIMEOUT_SECONDS", "8"))

View File

@@ -2,8 +2,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from .agents import AfterSalesAgent, PreSalesAgent, QuoteAgent, RiskAgent, RouterAgent from .agents import AfterSalesAgent, PreSalesAgent, QuoteAgent, RiskAgent
from .config import FAST_ROUTE_ENABLED
from .image_quote_analyzer import analyze_image_for_quote from .image_quote_analyzer import analyze_image_for_quote
from .models import Decision from .models import Decision
from .state_machine import evolve_after_sales_state, migrate_state_schema from .state_machine import evolve_after_sales_state, migrate_state_schema
@@ -12,7 +11,6 @@ from .store import ConversationStore
class Orchestrator: class Orchestrator:
def __init__(self) -> None: def __init__(self) -> None:
self.router = RouterAgent()
self.pre_sales = PreSalesAgent() self.pre_sales = PreSalesAgent()
self.quote = QuoteAgent() self.quote = QuoteAgent()
self.after_sales = AfterSalesAgent() self.after_sales = AfterSalesAgent()
@@ -20,9 +18,7 @@ class Orchestrator:
self.store = ConversationStore() self.store = ConversationStore()
@staticmethod @staticmethod
def _fast_route(context: dict[str, Any]) -> tuple[str, str] | None: def _single_stage_route(context: dict[str, Any]) -> tuple[str, str]:
if not FAST_ROUTE_ENABLED:
return None
msg = str(context.get("msg", "") or "") msg = str(context.get("msg", "") or "")
lower = msg.lower() lower = msg.lower()
if bool(context.get("auto_quote_trigger")): if bool(context.get("auto_quote_trigger")):
@@ -34,7 +30,7 @@ class Orchestrator:
return "quote", "fast:pending_image_and_pricing" return "quote", "fast:pending_image_and_pricing"
if any(k in lower for k in ("退款", "退钱", "不满意", "重做", "售后", "返工", "重发")): if any(k in lower for k in ("退款", "退钱", "不满意", "重做", "售后", "返工", "重发")):
return "after_sales", "fast:after_sales_keyword" return "after_sales", "fast:after_sales_keyword"
return None return "pre_sales", "single_stage:default_pre_sales"
async def decide(self, context: dict[str, Any]) -> tuple[str, Decision, dict[str, Any]]: async def decide(self, context: dict[str, Any]) -> tuple[str, Decision, dict[str, Any]]:
customer_key = context["customer_key"] customer_key = context["customer_key"]
@@ -54,11 +50,7 @@ class Orchestrator:
"order_status": order_status, "order_status": order_status,
} }
fast = self._fast_route(merged_ctx) route, route_reason = self._single_stage_route(merged_ctx)
if fast is not None:
route, route_reason = fast
else:
route, route_reason = await self.router.route(merged_ctx)
if route == "quote": if route == "quote":
latest_image_url = str(context.get("latest_image_url", "") or "").strip() latest_image_url = str(context.get("latest_image_url", "") or "").strip()