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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user