fix: 修复 Agent 流式对话无响应和工具 schema 兼容性问题

- 在 `run_stream()` LLM 调用前 yield `think` 事件,前端即时显示"思考中..."
- 修复 tool schema 规范化逻辑:`{"function":{...}}` 格式缺少 `type` 字段导致 LLM API 拒绝
- 启动时从数据库加载自定义工具(`load_tools_from_db`),解决重启后工具丢失
- 前端 SSE 添加 60s 超时保护,任何事件类型均触发 `receivedFirstEvent`
- 流式失败自动降级到非流式 POST
- 添加 `scripts/seed_coding_agent.py` 和 `scripts/test_coding_agent.py`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-02 00:38:41 +08:00
parent 342f3fcb16
commit 7aba0f9bc5
10 changed files with 662 additions and 36 deletions

View File

@@ -21,6 +21,20 @@ _DSML_NO_ARG_TOOLS = {
}
def _chat_messages(
user_content: str,
system_prompt: Optional[str] = None,
) -> List[Dict[str, str]]:
"""构建 chat.completions messages:可选 system + user。"""
msgs: List[Dict[str, str]] = []
if system_prompt is not None:
sp = system_prompt.strip()
if sp:
msgs.append({"role": "system", "content": sp})
msgs.append({"role": "user", "content": user_content})
return msgs
def _resolve_request_timeout(kwargs: Dict[str, Any]) -> float:
"""
统一解析 LLM 请求超时时间(秒):
@@ -415,6 +429,7 @@ class LLMService:
max_tokens: Optional[int] = None,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
system_prompt: Optional[str] = None,
**kwargs
) -> str:
"""
@@ -470,9 +485,7 @@ class LLMService:
try:
response = await client.chat.completions.create(
model=model,
messages=[
{"role": "user", "content": prompt}
],
messages=_chat_messages(prompt, system_prompt),
temperature=temperature,
max_tokens=max_tokens,
timeout=request_timeout,
@@ -507,6 +520,7 @@ class LLMService:
max_tokens: Optional[int] = None,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
system_prompt: Optional[str] = None,
**kwargs
) -> str:
"""
@@ -566,9 +580,7 @@ class LLMService:
try:
response = await client.chat.completions.create(
model=model,
messages=[
{"role": "user", "content": prompt}
],
messages=_chat_messages(prompt, system_prompt),
temperature=temperature,
max_tokens=max_tokens,
timeout=request_timeout,
@@ -602,6 +614,7 @@ class LLMService:
model: Optional[str] = None,
temperature: float = 0.7,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
**kwargs
) -> str:
"""
@@ -627,6 +640,7 @@ class LLMService:
model=model,
temperature=temperature,
max_tokens=max_tokens,
system_prompt=system_prompt,
**kwargs
)
elif provider == "deepseek":
@@ -638,6 +652,7 @@ class LLMService:
model=model,
temperature=temperature,
max_tokens=max_tokens,
system_prompt=system_prompt,
**kwargs
)
else:
@@ -658,6 +673,7 @@ class LLMService:
on_tool_executed: Optional[Callable[[str], Awaitable[None]]] = None,
request_timeout: Optional[float] = None,
extra_body: Optional[Dict[str, Any]] = None,
system_prompt: Optional[str] = None,
) -> str:
"""
调用OpenAI API,支持工具调用
@@ -693,7 +709,7 @@ class LLMService:
raise ValueError("OpenAI API Key未配置")
client = self.openai_client
messages = [{"role": "user", "content": prompt}]
messages = _chat_messages(prompt, system_prompt)
request_timeout = _resolve_request_timeout(
{"request_timeout": request_timeout} if request_timeout is not None else {}
@@ -716,10 +732,11 @@ class LLMService:
openai_tools = []
for tool in tools:
if isinstance(tool, dict):
if "type" in tool and tool["type"] == "function":
if tool.get("type") == "function":
openai_tools.append(tool)
elif "function" in tool:
openai_tools.append(tool)
# Has function but missing type
openai_tools.append({"type": "function", "function": tool["function"]})
else:
# 假设是function格式,包装一下
openai_tools.append({
@@ -884,6 +901,7 @@ class LLMService:
on_tool_executed: Optional[Callable[[str], Awaitable[None]]] = None,
request_timeout: Optional[float] = None,
extra_body: Optional[Dict[str, Any]] = None,
system_prompt: Optional[str] = None,
) -> str:
"""
调用DeepSeek API,支持工具调用(DeepSeek兼容OpenAI API格式)
@@ -903,6 +921,7 @@ class LLMService:
on_tool_executed=on_tool_executed,
request_timeout=request_timeout,
extra_body=extra_body,
system_prompt=system_prompt,
)
async def call_llm_with_tools(
@@ -917,6 +936,7 @@ class LLMService:
tool_choice: Optional[str] = None,
on_tool_executed: Optional[Callable[[str], Awaitable[None]]] = None,
request_timeout: Optional[float] = None,
system_prompt: Optional[str] = None,
**kwargs
) -> str:
"""
@@ -947,6 +967,7 @@ class LLMService:
tool_choice=tool_choice,
on_tool_executed=on_tool_executed,
request_timeout=request_timeout,
system_prompt=system_prompt,
**kwargs
)
elif provider == "deepseek":
@@ -962,6 +983,7 @@ class LLMService:
tool_choice=tool_choice,
on_tool_executed=on_tool_executed,
request_timeout=request_timeout,
system_prompt=system_prompt,
**kwargs
)
else:

View File

@@ -1434,9 +1434,13 @@ class WorkflowEngine:
logger.debug(f"[rjb] LLM节点数据: node_id={node_id}, node_data keys={list(node_data.keys())}, api_key={'已配置' if node_data.get('api_key') else '未配置'}")
prompt = node_data.get('prompt', '')
# 如果prompt为空,使用默认提示词
# 如果 prompt 为空:不要用 {input} 展开整包 input_data。
# 预览/执行侧常带 user_id、memory、conversation_history 等大对象,模型易照抄成 ```json 回复。
if not prompt:
prompt = "请处理以下输入数据:\n{input}"
prompt = (
"请根据用户当前问题用自然语言回答;需要时可用工具。"
"不要向用户复述或输出完整的 input_data / API 请求 JSON。"
)
# 格式化prompt,替换变量
try:
@@ -1804,6 +1808,15 @@ class WorkflowEngine:
# 记录实际发送给LLM的prompt
logger.info(f"[rjb] 准备调用LLM: node_id={node_id}, provider={provider}, model={model}, prompt前200字符='{prompt[:200] if len(prompt) > 200 else prompt}'")
_raw_sys = node_data.get("system_prompt")
llm_system_prompt: Optional[str] = None
if isinstance(_raw_sys, str) and _raw_sys.strip():
llm_system_prompt = _raw_sys.strip()
elif _raw_sys is not None and not isinstance(_raw_sys, (dict, list)):
_ts = str(_raw_sys).strip()
if _ts:
llm_system_prompt = _ts
# 检查是否启用工具调用
enable_tools = node_data.get('enable_tools', False)
@@ -1872,6 +1885,7 @@ class WorkflowEngine:
execution_logger=self.logger,
tool_choice=_tool_choice,
on_tool_executed=self._on_tool_executed_budget,
system_prompt=llm_system_prompt,
**_merged_tool_kw,
)
result = self._enrich_llm_json_user_profile(result, input_data)
@@ -1882,6 +1896,7 @@ class WorkflowEngine:
model=model,
temperature=temperature,
max_tokens=max_tokens,
system_prompt=llm_system_prompt,
**llm_extra_kw,
)
result = self._enrich_llm_json_user_profile(result, input_data)