fix(core): prefer _llm_gateway over _llm_client in _handle_llm_generate

AgentPool.create_agent() injects only llm_gateway (v2 path) into
ConfigDrivenAgent and never sets the legacy _llm_client field. The
previous _handle_llm_generate implementation gated the entire LLM
call on `self._llm_client is None`, so every expert/board agent
created via the pool silently fell through to the
`llm_generate_no_client` placeholder — which is why the private
board moderator/experts reported "LLM 不可用" even when the gateway
and providers were healthy.

Mirror the same precedence used by `_handle_direct`: prefer
`_llm_gateway` (with agent_name/task_type for usage tracking), fall
back to `_llm_client` for backward compatibility, and only return
the no-client placeholder when both are unavailable.

Add a unit test that constructs an agent with `llm_gateway` only
(_llm_client is None) and asserts the gateway.chat path is taken.
This commit is contained in:
Chiguyong 2026-07-06 15:09:55 +08:00
parent 8063f4efe0
commit d0fe6611c7
2 changed files with 67 additions and 2 deletions

View File

@ -1358,7 +1358,14 @@ class ConfigDrivenAgent(BaseAgent, EvolutionMixin):
return gateway
async def _handle_llm_generate(self, task: TaskMessage) -> dict:
"""LLM 生成模式:渲染 Prompt → 调用 LLM → 解析输出"""
"""LLM 生成模式:渲染 Prompt → 调用 LLM → 解析输出
ponytail: 优先使用 ``_llm_gateway``v2 路径``AgentPool`` 注入的
仅在 gateway 缺失时回退到 ``_llm_client``legacy 字段原实现只检查
``_llm_client``导致 ``AgentPool`` 创建的 expert/board agent
``_llm_client is None`` ``_llm_gateway`` 已注入时错误降级为
``llm_generate_no_client``
"""
if not self._prompt_template:
raise ConfigValidationError(
agent_name=self.name,
@ -1371,7 +1378,19 @@ class ConfigDrivenAgent(BaseAgent, EvolutionMixin):
variables["task_type"] = task.task_type
messages = self._prompt_template.render(variables=variables)
# 调用 LLM
# v2 路径_llm_gateway 优先AgentPool 仅注入 gateway不设 _llm_client
if self._llm_gateway is not None:
llm_params = self._config.llm.copy() if self._config.llm else {}
model = llm_params.get("model", "default")
response = await self._llm_gateway.chat(
messages=messages,
model=model,
agent_name=self.name,
task_type=task.task_type,
)
return self._parse_llm_response(response.content)
# 兼容旧路径_llm_clientlegacy
if self._llm_client is None:
# 无 LLM 客户端时返回渲染后的 Prompt降级模式
return {

View File

@ -144,6 +144,52 @@ class TestConfigDrivenAgent:
assert result["mode"] == "llm_generate_no_client"
assert len(result["messages"]) == 2 # system + user
async def test_llm_generate_with_gateway_only(self):
"""仅注入 LLMGateway_llm_client=None时应正常调用 LLM。
复现AgentPool.create_agent() 只注入 llm_gateway不设 _llm_client
原实现错误降级为 llm_generate_no_client导致私董会专家返回占位文本
"""
class _StubResponse:
def __init__(self, content: str) -> None:
self.content = content
class _StubGateway:
def __init__(self) -> None:
self.calls: list[dict] = []
async def chat(self, *, messages, model, agent_name, task_type, **kwargs):
self.calls.append(
{
"messages": messages,
"model": model,
"agent_name": agent_name,
"task_type": task_type,
}
)
return _StubResponse(json.dumps({"title": "FromGateway", "content": "OK"}))
gateway = _StubGateway()
config = AgentConfig.from_dict(_sample_llm_config())
agent = ConfigDrivenAgent(config=config, llm_gateway=gateway)
# 验证前置条件_llm_client 必须为 None模拟 AgentPool 注入路径
assert agent._llm_client is None
assert agent._llm_gateway is gateway
task = _make_task()
result = await agent.handle_task(task)
# 不应再走 no_client 降级
assert "mode" not in result or result.get("mode") != "llm_generate_no_client"
assert result["title"] == "FromGateway"
assert result["content"] == "OK"
# 验证 gateway.chat 被实际调用
assert len(gateway.calls) == 1
assert gateway.calls[0]["agent_name"] == config.name
assert gateway.calls[0]["model"] == "gpt-4"
async def test_llm_generate_with_client(self):
"""有 LLM 客户端时调用 LLM 并解析结果"""