agent_registry.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """主协调及业务 Agent 按名称缓存、单飞编译与失效重建。
  2. 为什么要单例:``create_deep_agent`` 要编译整张 LangGraph 图,耗时在百毫秒
  3. 到秒级;每次请求都编译会让首 token 时间不可接受。
  4. 为什么要失效重建:图一旦编译,模型实例、思考级别、middleware 栈、
  5. compression 阈值都固化了。改了这些配置只能重建,不能原地改。
  6. 「单飞编译」(single-flight):并发请求同时发现缓存为空时,只让第一个
  7. 真正去编译,其余等它完成——否则冷启动瞬间会同时编译 N 次。
  8. """
  9. from __future__ import annotations
  10. import asyncio
  11. import threading
  12. from typing import Any
  13. from obs import get_logger
  14. logger = get_logger(__name__)
  15. class _AgentSlot:
  16. """持有编译好的 agent 及其代数(generation)。
  17. 代数用于判断「我拿到的是不是已经被失效过的那一版」——失效后旧实例
  18. 可能仍在服务在途请求,不能强行打断,只能让新请求拿新的。
  19. """
  20. def __init__(self, name: str = "main") -> None:
  21. self.name = name
  22. self._agent: Any | None = None
  23. self._generation = 0
  24. self._lock = threading.Lock()
  25. @property
  26. def generation(self) -> int:
  27. return self._generation
  28. def peek(self) -> Any | None:
  29. """快路径:已编译则直接返回,否则 None(不触发编译)。"""
  30. return self._agent
  31. def get(self) -> Any:
  32. """取 agent,必要时编译。并发调用只编译一次。"""
  33. agent = self._agent
  34. if agent is not None:
  35. return agent
  36. with self._lock:
  37. # 二次检查:可能在等锁期间已被别的线程编译好
  38. if self._agent is None:
  39. logger.debug("首次编译 agent(generation=%d)", self._generation + 1)
  40. self._agent = self._compile()
  41. self._generation += 1
  42. logger.info("agent 编译完成(generation=%d)", self._generation)
  43. return self._agent
  44. def _compile(self) -> Any:
  45. from agents.agent_factory import build_agent
  46. return build_agent() if self.name == "main" else build_agent(self.name)
  47. def invalidate(self) -> int:
  48. """丢弃当前实例,返回新代数。下次 ``get()`` 会重新编译。"""
  49. with self._lock:
  50. self._agent = None
  51. self._generation += 1
  52. logger.debug("agent 已失效,下次请求将重建(generation=%d)", self._generation)
  53. return self._generation
  54. _slot = _AgentSlot()
  55. _slots: dict[str, _AgentSlot] = {"main": _slot}
  56. _slots_lock = threading.Lock()
  57. def _named_slot(name: str) -> _AgentSlot:
  58. if name != "main":
  59. from extensions.registry import load_extensions
  60. load_extensions().business(name)
  61. with _slots_lock:
  62. return _slots.setdefault(name, _AgentSlot(name))
  63. def get_agent(name: str = "main") -> Any:
  64. """按名称取主 Agent 或业务 Agent,分别单飞编译和缓存。"""
  65. return _named_slot(name).get()
  66. def peek_agent(name: str = "main") -> Any | None:
  67. """快路径取 agent;未编译返回 None。热路径上先用它避免持锁。"""
  68. return _named_slot(name).peek()
  69. def invalidate(name: str | None = None) -> int:
  70. """使当前 agent 失效。改模型/思考级别/压缩配置后调用。"""
  71. from agents import model_factory
  72. if name is not None:
  73. return _named_slot(name).invalidate()
  74. model_factory.clear_cache()
  75. with _slots_lock:
  76. slots = list(_slots.values())
  77. for slot in slots:
  78. slot.invalidate()
  79. return _slot.generation
  80. def generation() -> int:
  81. """当前代数。"""
  82. return _slot.generation
  83. async def prewarm() -> bool:
  84. """后台预热:把耗时的图编译挪到线程池,不阻塞事件循环。
  85. 启动时调用。失败不抛——预热失败应当在首次真实请求时暴露,
  86. 而不是让整个服务起不来。
  87. """
  88. try:
  89. await asyncio.to_thread(get_agent)
  90. return True
  91. except Exception as exc:
  92. logger.exception("agent 预热失败,将在首次请求时重试", extra={"kv": {"event": "agent.prewarm_failed"}})
  93. return False