client.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import os
  2. import time
  3. import functools
  4. from langchain_openai import ChatOpenAI
  5. from app.monitoring import llm_calls_total, llm_duration_seconds, llm_tokens_total
  6. def _common_kwargs() -> dict:
  7. return {
  8. "api_key": os.getenv("LLM_API_KEY", os.getenv("OPENAI_API_KEY", "")),
  9. "base_url": os.getenv("LLM_BASE_URL", os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")),
  10. "timeout": 180,
  11. "max_retries": 1,
  12. }
  13. def _wrap_llm_ainvoke(llm: ChatOpenAI, model_name: str) -> ChatOpenAI:
  14. original_ainvoke = llm.ainvoke
  15. @functools.wraps(original_ainvoke)
  16. async def monitored_ainvoke(*args, **kwargs):
  17. llm_calls_total.labels(model=model_name, status="started").inc()
  18. start = time.perf_counter()
  19. try:
  20. result = await original_ainvoke(*args, **kwargs)
  21. elapsed = time.perf_counter() - start
  22. llm_duration_seconds.labels(model=model_name).observe(elapsed)
  23. llm_calls_total.labels(model=model_name, status="success").inc()
  24. try:
  25. usage = getattr(result, "response_metadata", {}).get("token_usage", {})
  26. if usage:
  27. prompt_tokens = usage.get("prompt_tokens", 0)
  28. completion_tokens = usage.get("completion_tokens", 0)
  29. total_tokens = usage.get("total_tokens", prompt_tokens + completion_tokens)
  30. llm_tokens_total.labels(model=model_name, type="prompt").inc(prompt_tokens)
  31. llm_tokens_total.labels(model=model_name, type="completion").inc(completion_tokens)
  32. llm_tokens_total.labels(model=model_name, type="total").inc(total_tokens)
  33. except Exception:
  34. pass
  35. return result
  36. except Exception as e:
  37. llm_duration_seconds.labels(model=model_name).observe(time.perf_counter() - start)
  38. llm_calls_total.labels(model=model_name, status="error").inc()
  39. raise
  40. try:
  41. # pydantic v2 models block arbitrary attribute assignment; fall back gracefully
  42. llm.ainvoke = monitored_ainvoke
  43. except (ValueError, AttributeError):
  44. pass
  45. return llm
  46. def get_llm() -> ChatOpenAI:
  47. # 优先 LLM_* 系列(与 .env.production 一致),兼容 OPENAI_* 系列
  48. # timeout=180/max_retries=1:问卷推理可达 30-60s,重试叠加会拖垮 gunicorn worker
  49. llm = ChatOpenAI(
  50. model=os.getenv("LLM_MODEL", os.getenv("OPENAI_MODEL", "gpt-4o-mini")),
  51. temperature=0.7,
  52. **_common_kwargs(),
  53. )
  54. return _wrap_llm_ainvoke(llm, llm.model_name)
  55. def get_vision_llm() -> ChatOpenAI:
  56. # 视觉模型(舌诊等图像分析),复用 LLM 网关与 key,仅切换模型名
  57. llm = ChatOpenAI(
  58. model=os.getenv("LLM_VISION_MODEL", "glm-5"),
  59. temperature=0.3,
  60. **_common_kwargs(),
  61. )
  62. return _wrap_llm_ainvoke(llm, llm.model_name)