test_fast_stream.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. # -*- coding: utf-8 -*-
  2. """
  3. Fast 模式流式输出测试 Demo
  4. 模拟 /api/interpret/click/tun?mode=fast 的完整 LLM 调用链路:
  5. 1. 读取 .env 配置 → 获取 deepseek-v4-flash 模型
  6. 2. 构造与 Fast 模式完全一致的 system + user prompt
  7. 3. 流式调用模型,逐 token 打印并统计延迟
  8. 用法:
  9. cd D:\\workspace\\py\\vent_deep_agent_1.4
  10. python tests/test_fast_stream.py
  11. 也可以带参数指定巷道 ID(会走 MCP 真实查询):
  12. python tests/test_fast_stream.py --tun-id 581 --tun-name "二水平中部辅运大巷"
  13. """
  14. import argparse
  15. import asyncio
  16. import json
  17. import os
  18. import sys
  19. import time
  20. from pathlib import Path
  21. # 确保项目根目录在 sys.path 中
  22. sys.path.insert(0, str(Path(__file__).parent.parent))
  23. from dotenv import dotenv_values
  24. from openai import AsyncOpenAI
  25. # ── 与 routes.py 完全一致的 Fast 模式系统提示词 ──
  26. FAST_TUN_SYSTEM_PROMPT = """你是一名煤矿通风安全专家。请根据提供的巷道监测数据,直接生成标准化结构化解读报告。
  27. ## 报告格式(严格按此模板输出,不要添加额外标题或代码块)
  28. **{巷道名称}**
  29. - 当前风速:{数值} m/s
  30. - 规程允许范围:{min} ~ {max} m/s
  31. - 当前风量:{数值} m³/min{,计划风量:{数值} m³/min}
  32. - 瓦斯浓度:{数值}%(限值 {限值}%)
  33. - CO浓度:{数值} ppm(限值 {限值} ppm)
  34. - 温度:{数值}℃(限值 {限值}℃)
  35. - 设备状态:{状态}
  36. - 结论:{正常/重大风险/高风险/中风险}。{详细异常分析}
  37. - 依据:《煤矿安全规程》相关条款
  38. - 建议:{针对性通风整改措施}
  39. ## 风险分级标准
  40. - 风速偏低:实测<0.5倍下限→重大;0.5倍≤实测<0.8倍下限→高;0.8倍≤实测<下限→中
  41. - 风速偏高:实测≥1.8倍上限→重大;1.4倍≤实测<1.8倍上限→高;上限≤实测<1.4倍上限→中
  42. - 风量超配:实测≥1.8倍计划→重大;1.5倍≤实测<1.8倍计划→高;计划<实测<1.5倍计划→中
  43. - 风量不足:实测<0.75倍计划→重大;0.75倍≤实测<0.9倍计划→高;0.9倍≤实测<计划→中
  44. ## 巷道类型编码对照
  45. - 0-回采工作面;1-掘进工作面;2-辅运巷;3-主运巷;4-硐室;5-联络巷;6-进风井;7-回风井;8-专用回风巷
  46. ## 规则
  47. - 所有分析必须基于提供的数据,绝不编造
  48. - 全程使用简体中文输出
  49. - 严格按照模板格式输出,不要添加额外标题、总结、代码块、JSON
  50. - 禁止暴露巷道ID,统一用巷道名称代替
  51. - 数据缺失项标注"无数据"并说明影响
  52. """
  53. # ── 模拟巷道数据(与 MCP 返回格式一致) ──
  54. MOCK_TUN_DATA = {
  55. "tunId": "581",
  56. "tunnelName": "二水平中部辅运大巷",
  57. "needAirVolume": 1200,
  58. "usingType": 2,
  59. "usingTypeName": "辅运巷",
  60. "permissibleMin": 0.25,
  61. "permissibleMax": 6.0,
  62. "permissibleVelocity": {
  63. "fmin": 0.25,
  64. "fmax": 6.0,
  65. },
  66. "devices": [
  67. {
  68. "sourceType": "wind",
  69. "sourceName": "二水平中部辅运大巷测风站",
  70. "installPos": "二水平中部辅运大巷中部",
  71. "airVolume": 1150.5,
  72. "windSpeed": 1.85,
  73. "warnFlag": 0,
  74. "netStatus": 1,
  75. "deviceStatusName": "在线",
  76. "readTime": "2026-07-15 10:30:00",
  77. "m3": 1150.5,
  78. "va": 1.85,
  79. "alarmDescription": "",
  80. "alarmDescriptions": [],
  81. }
  82. ],
  83. # 模拟瓦斯传感器
  84. "gasSensor": {
  85. "concentration": 0.12,
  86. "limit": 0.8,
  87. "unit": "%",
  88. },
  89. # 模拟 CO 传感器
  90. "coSensor": {
  91. "concentration": 5,
  92. "limit": 24,
  93. "unit": "ppm",
  94. },
  95. # 模拟温度传感器
  96. "tempSensor": {
  97. "value": 24.5,
  98. "limit": 30,
  99. "unit": "℃",
  100. },
  101. }
  102. def load_env():
  103. """读取 .env 配置(与 _get_chat_model 逻辑一致)"""
  104. env_file = Path(__file__).parent.parent / ".env"
  105. _cfg = dotenv_values(str(env_file))
  106. model_str = _cfg.get("SUMMARY_MODEL", "") or _cfg.get("DEEPAGENT_MODEL", "deepseek-v4-flash")
  107. base_url = _cfg.get("OPENAI_BASE_URL", "https://api.deepseek.com").strip().strip('"').strip("'")
  108. api_key = _cfg.get("OPENAI_API_KEY", "").strip().strip('"').strip("'")
  109. return model_str, base_url, api_key
  110. def build_prompt(tun_name: str, data: dict) -> list[dict]:
  111. """构造与 routes.py _fast_tun_sse_generator 完全一致的 messages"""
  112. data_str = json.dumps(data, ensure_ascii=False, indent=2)
  113. return [
  114. {"role": "system", "content": FAST_TUN_SYSTEM_PROMPT},
  115. {"role": "user", "content": f"请根据以下巷道监测数据生成标准化解读报告:\n\n巷道名称:{tun_name}\n\n数据:\n{data_str}"},
  116. ]
  117. async def stream_model(client: AsyncOpenAI, model: str, messages: list[dict]):
  118. """流式调用模型,逐 token 输出并计时"""
  119. t0 = time.perf_counter()
  120. first_token_time = None
  121. token_count = 0
  122. full_text = ""
  123. print("─" * 60)
  124. print(f"模型: {model}")
  125. print(f"System prompt 长度: {len(messages[0]['content'])} 字符")
  126. print(f"User prompt 长度: {len(messages[1]['content'])} 字符")
  127. print("─" * 60)
  128. print("\n[LLM 流式输出开始]\n")
  129. stream = await client.chat.completions.create(
  130. model=model,
  131. messages=messages,
  132. temperature=0,
  133. stream=True,
  134. stream_options={"include_usage": True},
  135. )
  136. async for chunk in stream:
  137. if chunk.choices and chunk.choices[0].delta.content:
  138. content = chunk.choices[0].delta.content
  139. if first_token_time is None:
  140. first_token_time = time.perf_counter()
  141. ttft = first_token_time - t0
  142. print(f"\n{'─' * 40}")
  143. print(f"⏱ 首 token 延迟 (TTFT): {ttft:.2f}s")
  144. print(f"{'─' * 40}\n")
  145. token_count += 1
  146. full_text += content
  147. # 实时逐 token 打印(不换行)
  148. print(content, end="", flush=True)
  149. t_total = time.perf_counter() - t0
  150. ttft = (first_token_time - t0) if first_token_time else t_total
  151. print("\n")
  152. print("─" * 60)
  153. print(f"📊 统计:")
  154. print(f" 首 token 延迟: {ttft:.2f}s")
  155. print(f" 总耗时: {t_total:.2f}s")
  156. print(f" 总 token 数: {token_count}")
  157. print(f" 平均速度: {token_count / max(t_total - ttft, 0.01):.1f} tok/s")
  158. print(f" 输出长度: {len(full_text)} 字符")
  159. print("─" * 60)
  160. return full_text
  161. async def main():
  162. parser = argparse.ArgumentParser(description="Fast 模式流式输出测试")
  163. parser.add_argument("--tun-id", default="581", help="巷道 ID(默认 581,使用 mock 数据)")
  164. parser.add_argument("--tun-name", default="二水平中部辅运大巷", help="巷道名称")
  165. parser.add_argument("--real-data", action="store_true", help="通过 MCP 获取真实数据(需要 MCP 服务可用)")
  166. parser.add_argument("--model", default=None, help="覆盖模型名称(默认从 .env 读取 SUMMARY_MODEL)")
  167. args = parser.parse_args()
  168. # 1. 加载环境配置
  169. model_str, base_url, api_key = load_env()
  170. if args.model:
  171. model_str = args.model
  172. print(f"🔧 API Base: {base_url}")
  173. print(f"🔧 模型: {model_str}")
  174. if not api_key:
  175. print("❌ 未找到 OPENAI_API_KEY,请检查 .env 文件")
  176. return
  177. # 2. 获取数据
  178. if args.real_data:
  179. print("🌐 通过 MCP 获取真实数据...")
  180. t_mcp_start = time.perf_counter()
  181. from tools.vent_tools import query_tun_data_by_id
  182. raw = await query_tun_data_by_id(args.tun_id)
  183. data = json.loads(raw) if isinstance(raw, str) else raw
  184. t_mcp = time.perf_counter() - t_mcp_start
  185. print(f" MCP 耗时: {t_mcp:.2f}s")
  186. else:
  187. print("📦 使用 Mock 数据(跳过 MCP 调用)")
  188. data = MOCK_TUN_DATA
  189. # 3. 构造提示词
  190. messages = build_prompt(args.tun_name, data)
  191. # 4. 初始化 OpenAI 客户端
  192. client = AsyncOpenAI(
  193. api_key=api_key,
  194. base_url=base_url + "/v1" if not base_url.endswith("/v1") else base_url,
  195. timeout=180.0,
  196. max_retries=2,
  197. )
  198. # 5. 流式调用
  199. full_text = await stream_model(client, model_str, messages)
  200. # 6. 可选:保存结果
  201. output_file = Path(__file__).parent / "fast_test_output.txt"
  202. output_file.write_text(full_text, encoding="utf-8")
  203. print(f"\n💾 完整输出已保存到: {output_file}")
  204. if __name__ == "__main__":
  205. asyncio.run(main())