config_lint.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. """
  2. config_lint.py — 配置静态检查工具
  3. ==================================
  4. 为 Critic 和 Optimizer agent 提供配置验证能力。
  5. 复用 lambdagent 的 schema 验证,并添加 builder 专用检查。
  6. """
  7. from __future__ import annotations
  8. import sys
  9. from pathlib import Path
  10. from typing import Dict, List, Tuple
  11. import yaml
  12. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
  13. sys.path.insert(0, str(PROJECT_ROOT))
  14. # ── 常量 ──────────────────────────────────────────────────
  15. VALID_TYPES = {"react", "chain", "router", "parallel"}
  16. VALID_PROVIDERS = {
  17. "anthropic", "openai", "dashscope", "deepseek",
  18. "zhipu", "moonshot", "ollama",
  19. }
  20. PROVIDER_MODELS = {
  21. "anthropic": {"claude-sonnet-4-20250514", "claude-opus-4-20250514"},
  22. "openai": {"gpt-4o", "gpt-4o-mini"},
  23. "dashscope": {"qwen3-max-2026-01-23", "qwen-max", "qwen-plus"},
  24. "deepseek": {"deepseek-chat", "deepseek-reasoner"},
  25. "zhipu": {"glm-4-plus", "glm-4-flash"},
  26. "moonshot": {"moonshot-v1-128k"},
  27. "ollama": {"llama3", "qwen2"},
  28. }
  29. VALID_LOCAL_TOOLS = {
  30. "terminate", "shell", "file", "browser", "app",
  31. "system", "screenshot", "done",
  32. }
  33. REQUIRED_FIELDS = ["agentId", "name", "type", "systemPrompt", "model"]
  34. # ── 检查函数 ──────────────────────────────────────────────
  35. def lint_config(config: Dict | str) -> Dict:
  36. """
  37. 对 agent 配置进行静态检查。
  38. Args:
  39. config: 配置字典或 YAML 字符串
  40. Returns:
  41. {
  42. "valid": bool,
  43. "errors": [{"level": "error", "rule": "R001", "message": "..."}],
  44. "warnings": [{"level": "warning", "rule": "W001", "message": "..."}],
  45. "score": int # 0-100 质量分
  46. }
  47. """
  48. if isinstance(config, str):
  49. try:
  50. config = yaml.safe_load(config)
  51. except yaml.YAMLError as e:
  52. return {
  53. "valid": False,
  54. "errors": [{"level": "error", "rule": "R000", "message": f"YAML 解析失败: {e}"}],
  55. "warnings": [],
  56. "score": 0,
  57. }
  58. if not isinstance(config, dict):
  59. return {
  60. "valid": False,
  61. "errors": [{"level": "error", "rule": "R000", "message": "配置不是有效的字典"}],
  62. "warnings": [],
  63. "score": 0,
  64. }
  65. errors: List[Dict] = []
  66. warnings: List[Dict] = []
  67. # R001: 必填字段检查
  68. for field in REQUIRED_FIELDS:
  69. if field not in config:
  70. errors.append({
  71. "level": "error", "rule": "R001",
  72. "message": f"缺少必填字段: {field}",
  73. })
  74. # R002: type 合法性
  75. agent_type = config.get("type")
  76. if agent_type and agent_type not in VALID_TYPES:
  77. errors.append({
  78. "level": "error", "rule": "R002",
  79. "message": f"无效的 type: {agent_type},合法值: {VALID_TYPES}",
  80. })
  81. # R003: model 配置
  82. model = config.get("model", {})
  83. if isinstance(model, dict):
  84. provider = model.get("provider")
  85. model_name = model.get("name")
  86. if provider and provider not in VALID_PROVIDERS:
  87. errors.append({
  88. "level": "error", "rule": "R003",
  89. "message": f"无效的 provider: {provider},合法值: {VALID_PROVIDERS}",
  90. })
  91. if provider and model_name:
  92. valid_models = PROVIDER_MODELS.get(provider, set())
  93. if valid_models and model_name not in valid_models:
  94. warnings.append({
  95. "level": "warning", "rule": "W003",
  96. "message": f"model {model_name} 不在 {provider} 的已知模型列表中: {valid_models}",
  97. })
  98. temp = model.get("temperature")
  99. if temp is not None and (temp < 0.0 or temp > 2.0):
  100. errors.append({
  101. "level": "error", "rule": "R003",
  102. "message": f"temperature {temp} 超出范围 [0.0, 2.0]",
  103. })
  104. # R004: type 与子配置匹配
  105. if agent_type == "react" and "react" not in config:
  106. warnings.append({
  107. "level": "warning", "rule": "W004",
  108. "message": "type=react 但缺少 react 配置块(将使用默认值)",
  109. })
  110. if agent_type == "chain" and "chain" not in config:
  111. errors.append({
  112. "level": "error", "rule": "R004",
  113. "message": "type=chain 必须有 chain 配置块",
  114. })
  115. if agent_type == "router" and "router" not in config:
  116. errors.append({
  117. "level": "error", "rule": "R004",
  118. "message": "type=router 必须有 router 配置块",
  119. })
  120. # R005: systemPrompt 质量
  121. prompt = config.get("systemPrompt", "")
  122. if isinstance(prompt, str):
  123. if len(prompt.strip()) < 20:
  124. errors.append({
  125. "level": "error", "rule": "R005",
  126. "message": f"systemPrompt 过短({len(prompt.strip())} 字符),至少 20 字符",
  127. })
  128. elif len(prompt.strip()) < 50:
  129. warnings.append({
  130. "level": "warning", "rule": "W005",
  131. "message": f"systemPrompt 较短({len(prompt.strip())} 字符),建议 100+ 字符",
  132. })
  133. # R006: mcp.localTools 必须包含 terminate
  134. mcp = config.get("mcp", {})
  135. if isinstance(mcp, dict):
  136. local_tools = mcp.get("localTools", [])
  137. if isinstance(local_tools, list) and "terminate" not in local_tools:
  138. warnings.append({
  139. "level": "warning", "rule": "W006",
  140. "message": "mcp.localTools 缺少 terminate(agent 可能无法正常结束)",
  141. })
  142. # 检查工具名合法性
  143. for tool in local_tools:
  144. if tool not in VALID_LOCAL_TOOLS and not tool.startswith("call_"):
  145. warnings.append({
  146. "level": "warning", "rule": "W006",
  147. "message": f"未知的 localTool: {tool}",
  148. })
  149. # R007: onlineTool 必须有对应的 app.mcp.custom.nodes
  150. online_tools = mcp.get("onlineTool", {}) if isinstance(mcp, dict) else {}
  151. if online_tools:
  152. app_nodes = _nested_get(config, "app", "mcp", "custom", "nodes") or {}
  153. for server_name in online_tools:
  154. if server_name not in app_nodes:
  155. errors.append({
  156. "level": "error", "rule": "R007",
  157. "message": f"onlineTool 引用 {server_name} 但 app.mcp.custom.nodes 中缺少其配置",
  158. })
  159. # W008: react.maxSteps 合理性
  160. react_config = config.get("react", {})
  161. if isinstance(react_config, dict):
  162. max_steps = react_config.get("maxSteps", 10)
  163. if max_steps > 30:
  164. warnings.append({
  165. "level": "warning", "rule": "W008",
  166. "message": f"react.maxSteps={max_steps} 过大,可能导致 token 浪费",
  167. })
  168. if max_steps < 2:
  169. warnings.append({
  170. "level": "warning", "rule": "W008",
  171. "message": f"react.maxSteps={max_steps} 过小,agent 可能无法完成任务",
  172. })
  173. # W009: guard 缺失
  174. if "guard" not in config:
  175. warnings.append({
  176. "level": "warning", "rule": "W009",
  177. "message": "缺少 guard 配置(建议至少设置 maxOutputLength)",
  178. })
  179. # ── 计算总分 ──
  180. score = 100
  181. score -= len(errors) * 15
  182. score -= len(warnings) * 5
  183. score = max(0, min(100, score))
  184. return {
  185. "valid": len(errors) == 0,
  186. "errors": errors,
  187. "warnings": warnings,
  188. "score": score,
  189. }
  190. def _nested_get(d: dict, *keys):
  191. for k in keys:
  192. if isinstance(d, dict):
  193. d = d.get(k)
  194. else:
  195. return None
  196. return d