Files
group_fqcd_jr/demo.ps1
张胜宇 2a55269e20 feat(W34-W36): 客服双通道口径分离收口 + 会签 20/21/22 落地 + 演示启动器修复
W34 · 会签 20/21/22 三项落地(先立单、经授权、后动手)
- 会签 20(白名单外):runtime_config_service 新增 load_collection_routes() /
  collection_routes() / _first_collection_name(),首次消费既有 JSON 列 collection_routes;
  消费方 customer_service 走「配置优先、缺失回落代码常量」。零 DDL;该列当前全为 None
  ⇒ 实际走回落路径,行为与改动前一致。
- 会签 21(类 3 + 融合层):retrieval_fusion 新增 fuse_rrf() + RRF_K(排名融合,只吃名次
  不吃分数 ⇒ 异质分数不可能污染判定分,best_vector_score 仍只取向量路原始 cosine);
  knowledge_search_service::search() 新增 literal_parallel: bool = False(默认值使行为
  逐字等同现状)。工具层透传未做 —— 那需改 KnowledgeSearchInput 契约(extra="forbid"),
  超出本单范围。
- 会签 22(发布配置 + bootstrap):customer_service 新增 INTENT_MARKET_QUOTE 常量 +
  TREND_WHITELIST_INTENT_CANDIDATES(按优先级回落)+ _trend_whitelist_intent()
  (运行时自检 + 自动回落,强于「仅报错」)。发布配置 customer_service:market_quote
  (release 260,allowed_tools=['query_fund_trend'])已写入并回读校验(9 → 10 行)。
  刻意未加入 supported_intents:授权维度与判定维度解耦,不动判定分布。
- 阶段 0:customer_service_rules 新增 normalize_query() + QUERY_SYNONYMS + 等级代号大写
  (纯函数;同义表只收纯书写差异,语义类同义留待金标 A/B 后逐条加;调用方默认不启用)。

W35 · 判定口径与融合口径分离(修 A-01 / C-04 / I-02 / E-04 四条)
- _dual_route_output 返回值新增 vector_order(向量路原始 doc_id 顺序、去重);
- 新增 _vector_decision_hits() 据此还原「判定序列」(带向量分的 basic 补位块回补首位;
  无 vector_order / 空 / id 全对不上 ⇒ 返回 None 回落原分支);
- _answer_from_knowledge 的 score / gap 与原文直返的 best 改从向量路原始序列取。
  语义边界(刻意):_evidence_pack / _exit_clarify / _answer_from_evidence 仍吃融合序列
  —— 融合的收益只留在「给哪些块、什么顺序」,符合三层分数分离约束。

W36 · 选块口径归一(收口最后一条 E-01)
- 新增 _pack_order():order 命中的块排前,其余按原相对顺序追加在后;
- _evidence_pack 新增 order= 参数,三处遍历 hits → ordered,top 由 hits[0] → ordered[0];
- _answer_from_knowledge 传入 order=[judge 的 doc_id 序列];judge is None ⇒ None
  (开关关闭时逐字零改动)。order 只当排序键、不当过滤器 ⇒ 证据包成员集合不变。

演示环境与文档
- start.ps1 / demo.ps1 默认端口 8000 → 8099(与 README / docs/06,07,09,14,15,32 /
  tools/smoke_check.py / login_console.py 的全仓口径对齐;字节级定长替换,保住
  UTF-8 BOM + CRLF,字节数不变);
- portal/README.md 更正 fin_nav_history 过期口径(「0 行」→ 实测 2494 行 / 20 个产品 /
  nav_date 覆盖 2026-03-18—2026-09-13)。

测试(新增 3 个文件、补强 2 个)
- 新增 tests/unit/service/test_decision_scope_w35.py(10 条)、
  tests/unit/service/test_evidence_pack_order_w36.py(13 条)、
  tests/unit/service/test_customer_service_trend_chart_inv8.py(INV-8 字面级判定,
  纳入 pytest 门禁,此前只在 jsdom 脚本里覆盖);
- 补强 tests/unit/core/test_customer_service_rules.py(normalize_query 7 条)与
  tests/integration/test_customer_service_trend_chart_persistence.py(图内每个数字
  都必须在答复正文出现过,判定口径与 INV-8 单测一致)。

验证
- 全量 pytest:2642 passed / 3 skipped / 0 failed(基线 2629 + 新增 13);
- 55 条金标真实链路 A/B 四组:off / norm / dual / on 均 55/55 = 100%
  (改前 dual 92.7%、on 90.9%);M-4 事实正确率恒 100%、M-7—M-10 全 0;
- 红线四条守住:融合/精排层仍不持 Milvus 客户端(INV-1)、阈值一字未动、零 DDL;
- 三个实验开关 CS_DUAL_ROUTE / CS_RERANK / CS_QUERY_NORM 仍默认关闭。
2026-09-22 18:05:59 +08:00

221 lines
10 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
客服 Agent 演示 · 前端一键启动(服务拉起 + 五项自检 + 打开演示页面)
.DESCRIPTION
真正的服务拉起交给同目录的 `start.ps1`(**幂等**:缺什么补什么,重复执行不会起出
第二个 API/Worker)。本脚本在它之上补三件事:
1. **刷新行情** —— 下单要求行情快照落在 **15 分钟**有效期内
(`app/service/trade_service.py` 的 `MAX_QUOTE_AGE`),过期后所有委托直接
503「行情已过期」,而系统**没有自动刷新机制**;所以每次启动都刷一次。
2. **五项自检** —— 口径与 `客服agent\D2.5` §1 一致(容器 / 向量库 / Worker / 行情 / 向量库开关)。
3. **打开前端页面** —— 访客页(客服浮窗主战场)与客户登录页。
⚠️ **登录限流**:`.env` 的 `RATE_LIMIT_*` 是 **10 次 / 60 秒**。演示时不要反复登录 ——
连续登录会让后续用例整片报红,看起来像「客服坏了」,其实是限流。
.PARAMETER Port
API 端口,默认 8099(与 `start.ps1 -Port` 同义)。
.PARAMETER SkipStart
不调 `start.ps1`,只做自检 + 开页面(服务已经在跑时用)。
.PARAMETER NoBrowser
不自动打开浏览器。
.PARAMETER KeepPrices
不刷新行情(只看自检结果时用)。
.EXAMPLE
双击 `启动演示.bat`
powershell -ExecutionPolicy Bypass -File demo.ps1 -SkipStart -NoBrowser
.NOTES
本文件必须保存为 **UTF-8 with BOM**(理由同 `start.ps1`:缺 BOM 时 Windows PowerShell 5.1
按 ANSI 解析,中文会乱码并抛语法错误)。
#>
param(
[int]$Port = 8099,
[switch]$SkipStart,
[switch]$NoBrowser,
[switch]$KeepPrices
)
$ErrorActionPreference = "Continue"
$Root = $PSScriptRoot
Set-Location $Root
$Python = Join-Path $Root ".venv\Scripts\python.exe"
if (-not (Test-Path $Python)) { $Python = "python" }
$Base = "http://127.0.0.1:$Port"
$script:Fails = 0
function Section($text) { Write-Host ""; Write-Host "--- $text ---" -ForegroundColor Cyan }
function Ok($text) { Write-Host " [OK] $text" -ForegroundColor Green }
function Warn($text) { Write-Host " [警告] $text" -ForegroundColor Yellow; $script:Fails++ }
function Bad($text) { Write-Host " [失败] $text" -ForegroundColor Red; $script:Fails++ }
function Test-ApiRunning {
try {
$null = Invoke-WebRequest -Uri "$Base/internal/health/ready" -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop
return $true
} catch { return $false }
}
function Wait-ApiReady {
for ($i = 1; $i -le 30; $i++) {
if (Test-ApiRunning) { return $true }
Start-Sleep -Seconds 1
}
return $false
}
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " 客服 Agent 演示 · 前端一键启动" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host "工作目录:$Root"
Write-Host "Python :$Python"
$apiWasRunning = Test-ApiRunning
# ---------------------------------------------------------------- 0. 拉起服务
if ($SkipStart) {
Section "0 服务拉起(-SkipStart:跳过 start.ps1)"
if ($apiWasRunning) { Ok "API 已在跑,直接进入自检" } else { Bad "API 未就绪,且按 -SkipStart 未拉起服务" }
} else {
Section "0 服务拉起(调用 start.ps1,幂等)"
& (Join-Path $Root "start.ps1") -Port $Port -NoBrowser -SkipPriceSync
if ($LASTEXITCODE -eq 1) { Bad "start.ps1 判定依赖缺失(常见:MySQL 未启动)—— 详见上方输出" }
}
Section "等待 API 就绪"
if (Wait-ApiReady) { Ok "API 已就绪:$Base/internal/health/ready" }
else { Bad "等了 30 秒仍未就绪:切到 API 窗口看报错(常见原因是数据库连不上)" }
# ---------------------------------------------------------------- 1~5. 五项自检
Section "自检 1/5 · 容器运行时(Milvus 三件套)"
if (Get-Command docker -ErrorAction SilentlyContinue) {
$containers = @(& docker ps --format "{{.Names}} {{.Status}}" 2>$null)
$milvus = @($containers | Where-Object { $_ -like "*milvus*" })
if ($milvus.Count -ge 3) { Ok "milvus 容器 $($milvus.Count) 个在跑:$($milvus -join " | ")" }
elseif ($milvus.Count -gt 0) { Warn "只看到 $($milvus.Count) 个 milvus 容器(期望 3 个:standalone / etcd / minio)" }
else { Warn "没看到 milvus 容器:知识检索会静默降级,先起 Docker Desktop" }
} else {
Warn "没找到 docker 命令,本项跳过"
}
Section "自检 2/5 · 向量库可查(四个集合计数)"
$probe = @'
import sys
sys.path.insert(0, '.')
from app.core.config import get_settings as g
from pymilvus import MilvusClient
client = MilvusClient(uri=g().resolved_milvus_uri)
for name in ("fin_faq_collection", "fin_product_collection", "fin_policy_collection", "fin_basic_collection"):
print(name, client.query(collection_name=name, filter='', output_fields=['count(*)']))
'@
# 经 stdin 送给 python,不走命令行参数:PowerShell 5.1 传原生命令参数时会吞掉内嵌双引号,
# 而这段探测代码必须保留字符串字面量(集合名与 output_fields)。
$probeOut = $probe | & $Python - 2>&1
if ($LASTEXITCODE -eq 0 -and (($probeOut | Out-String) -like "*count*")) {
Ok "四个集合可查(含 fin_basic_collection 补充语料):"
$probeOut | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
} else {
Warn "向量库探测失败:知识题会走 E5b 兜底(答不出但不会编)"
$probeOut | Select-Object -Last 4 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
}
Section "自检 3/5 · Agent Worker 在跑"
$pythonProcs = Get-CimInstance Win32_Process -Filter "Name like '%python%'" -ErrorAction SilentlyContinue
$workerProcs = @($pythonProcs | Where-Object { $_.CommandLine -like "*app.worker*" })
if ($workerProcs.Count -gt 0) { Ok "Worker 在跑(PID $($workerProcs[0].ProcessId))" }
else { Bad "没有 Worker:客服对话会一直 queued,前端显示「繁忙 / 超时」" }
Section "自检 4/5 · 行情有效期(下单前置,15 分钟)"
if ($KeepPrices) {
Warn "按 -KeepPrices 跳过刷新:若距上次刷新超过 15 分钟,下单会 503"
} else {
$syncOut = & $Python "tools\sync_market_prices.py" 2>&1
if ($LASTEXITCODE -eq 0) {
Ok "行情已刷新,15 分钟窗口从此刻重新计时"
$syncOut | Where-Object { $_ -like "*落库*" } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
} else {
Warn "行情刷新失败(退出码 $LASTEXITCODE):下单可能返回 503"
$syncOut | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
}
}
Section "自检 5/5 · 本地向量库开关(应为空:非空会绕过服务端 Milvus)"
$switchOut = & $Python -c "import sys; sys.path.insert(0, '.'); from app.core.config import get_settings as g; print(repr(g().milvus_local_uri)); print(g().resolved_milvus_uri)" 2>&1
if ($LASTEXITCODE -eq 0) {
$localUri = ($switchOut | Select-Object -First 1)
if ($localUri -eq "''") { Ok "milvus_local_uri 为空(符合预期);resolved = $($switchOut | Select-Object -Last 1)" }
else { Warn "milvus_local_uri 不为空:$localUri —— 演示前请清空该开关" }
} else {
Warn "配置读取失败"
$switchOut | Select-Object -Last 4 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
}
# ---------------------------------------------------------------- 演示入口
$guestUrl = "$Base/portal/guest/home/"
$customerUrl = "$Base/portal/customer/login/"
$staffUrl = "$Base/portal/employee-console/login/"
Section "演示入口"
Write-Host " 访客页(客服浮窗主战场) $guestUrl"
Write-Host " 客户登录 $customerUrl"
Write-Host " 员工登录(风控/运营/管理员)$staffUrl"
Write-Host " 投顾工作台 $Base/portal/employee-advisor/dashboard/"
Write-Host " 接口文档 $Base/docs"
if ($NoBrowser) {
Write-Host " (-NoBrowser:未自动打开浏览器)" -ForegroundColor DarkGray
} elseif (Test-ApiRunning) {
try {
Start-Process $guestUrl | Out-Null
Start-Sleep -Milliseconds 600
Start-Process $customerUrl | Out-Null
Ok "已打开两个页面(访客页 / 客户登录)"
} catch {
Warn "自动打开浏览器失败,请手动访问上面的地址"
}
}
# ---------------------------------------------------------------- 账号与提醒
Section "演示账号(实测可登录)"
Write-Host " 客户 cust_t / 123456"
Write-Host " 风控专员 risk_t / 666666"
Write-Host " 管理员 admin_t / 88888888"
Write-Host " 运营 offsite_t / offsite123"
Write-Host " 投顾 advisor_t / abc12345 (2026-09-20 恢复)"
Write-Host " review_t :登录返回 401 是设计如此(只作权限夹具)" -ForegroundColor DarkGray
Section "开讲前必看"
Write-Host " · 登录限流 10 次 / 60 秒:不要反复登录,否则后续整片报红(假故障)"
Write-Host " · 行情 15 分钟:中途下单报 503 就在新窗口重跑"
Write-Host " & `"$Python`" tools\sync_market_prices.py"
Write-Host " · 演示台词与话术:客服agent\D2.5-客服Agent演示脚本与账号速查-2026-09-19.md"
Write-Host " · 答辩问答准备 :客服agent\D2.6-客服Agent答辩报告-2026-09-19.md"
if ($apiWasRunning -and -not $SkipStart) {
Section "提示:服务已在跑,本次没有新开日志窗口"
Write-Host " 当前 API/Worker 可能是无窗口方式启动的。想要可见日志窗口,先停掉再双击本脚本:"
$stopCmd = @'
Get-CimInstance Win32_Process -Filter "Name like '%python%'" | Where-Object { $_.CommandLine -match 'uvicorn app.main:app|app.worker' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
'@
Write-Host $stopCmd -ForegroundColor DarkGray
}
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
if ($script:Fails -eq 0) {
Write-Host " 五项自检全过,演示环境就绪。" -ForegroundColor Green
} else {
Write-Host " 自检有 $script:Fails 项需要处理,见上方 [警告] / [失败] 行。" -ForegroundColor Yellow
}
Write-Host " 服务在独立窗口里运行,关掉本窗口不影响它们。" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
if ($script:Fails -gt 0) { exit 1 } else { exit 0 }