标签: AI助手

  • AI-Analyze:五个不显而易见的设计决策

    大部分代码分析工具的文章会讲"支持多少语言、有多少规则、能检测多少漏洞"。ai-analyze 支持 7 种语言、12 条安全规则、4 维度质量评分,这些功能数字在 README 里就能看到。这篇文章不讲功能清单,讲五个容易被忽略但值得深挖的设计决策——每个决策背后都有一个"为什么不那么做"的故事。

    决策一:MCP 双策略不是为了性能,是为了安全边界

    ai-analyze 实现了两种 MCP 集成策略。表面上看,直接调用 Serena 的 Python API(进程内导入)更快更简单,协议合规的 stdio 客户端(JSON-RPC 2.0 子进程通信)更慢更麻烦。选哪个似乎是个性能问题。

    但真正的关键差异不在性能,在安全边界 。直接调用策略暴露了 Serena 的全部工具——包括 rename_symbolreplace_symbol_body 这些修改源码的操作。你的代码分析器能重命名符号、替换函数体,这意味着一个误判的分析结果可能直接改坏你的代码。stdio 客户端刻意不实现 这两个修改操作。原因是:子进程边界天然隔离了写操作。stdin/stdout 管道只能传查询请求和返回结果,无法跨进程修改文件系统。这是一个用架构来约束能力的设计——不是"我不想实现",是"我不应该让你能做这件事"。

    class SerenaStdioClient:
        """MCP JSON-RPC 2.0 over stdio transport"""
    
        async def connect(self):
            self.process = await asyncio.create_subprocess_exec(
                *self.server_command,
                stdin=asyncio.subprocess.PIPE,
                stdout=asyncio.subprocess.PIPE,
            )
    
        async def send_request(self, method, params=None):
            request = {
                "jsonrpc": "2.0",
                "id": self._next_id,
                "method": method,
                "params": params or {}
            }
            self.process.stdin.write(json.dumps(request).encode() + b"\n")
            await self.process.stdin.drain()
            response = await self.process.stdout.readline()
            return json.loads(response.decode())
    

    两种策略并存的核心判断是:你的 MCP 服务是给自己用的,还是给别人用的?给自己用,直接调用更高效,你能控制自己不做危险操作。给别人用,协议合规是必须的——你不能假设别人的 Agent 会正确使用修改能力,用架构边界把它限制掉比用文档约束更可靠。

    决策二:合并层以 AST 为骨架,Serena 只是补充字段

    Serena 提供符号结构——类继承、函数调用链、跨文件引用关系。AST 分析器提供复杂度评分、代码味道检测、参数分析、async/static 标记。两个数据源合并时,以谁为主?

    直觉可能选 Serena——符号结构是代码的骨架,复杂度和味道是属性。但 ai-analyze 的 UnifiedAnalyzer 选择了 AST 为骨架。原因是:AST 产出的是可操作的指标 ——"这个函数的认知复杂度是 15,超过了阈值 10"是可以直接行动的发现。"这个类被 3 个模块引用"是结构信息,但无法告诉你该做什么。合并逻辑是以 AST 的文件列表为主循环,Serena 的符号数据作为补充属性挂上去。

    更有趣的是,UnifiedSymbol 数据类有一个 serena_data 字段,但合并代码对它采用了按需填充的策略——只有当 Serena 报告中存在同名同类型的符号时才填充,找不到时保持 None。这意味着 AST 分析永远不依赖 Serena 数据的可用性:有 Serena 时补充结构信息,没有时 AST 产出的指标依然完整。这不是 bug——这是容错设计。Serena 数据是增强点,不是依赖点。

    这个决策传达的设计思路是:数据合并的主循环应该围绕可操作的维度,而不是结构维度。结构信息提供上下文,指标信息驱动决策。

    决策三:AI 提示词里加了"反炒作"指令

    ai-analyze 有三个 AI 调用:代码质量评估、Docker 策略建议、框架升级分析。每个调用的提示词都把 AST 分析产出的结构化事实嵌入进去——Top 5 复杂函数、Top 10 代码味道、Top 20 依赖关系。AI 不是被要求去发现复杂度问题,而是被 handed 已确认的事实去解读。这种"规则先行、AI 解读"的模式降低了幻觉风险——AI 不能声称"这个项目没有复杂度问题",因为提示词里明确列出了 5 个复杂度超过 10 的函数。

    AI 调用基于 litellm,支持 100+ LLM 提供商——OpenAI、DeepSeek、Anthropic、Groq 等。切换提供商只需改一个环境变量 OPENAI_MODEL=deepseek/deepseek-chat,不需要改代码。但不管底层是哪个模型,提示词工程的原则不变。

    最不显而易见的设计决策在框架升级分析的提示词里。有一句显式的指令:

    "如果当前版本已经是最新稳定版,请明确说明,不要强求升级"

    这句指令在对抗 LLM 的一个固有倾向:总是建议改动 。LLM 培训数据里充满了"升级到最新版本"、"迁移到新框架"的建议,因为技术文章和 Stack Overflow 答案天然偏向推荐改变。但现实场景里,一个 React 18 项目不需要升级到 React 19 RC,一个 Python 3.11 项目不需要升级到 3.12。不加这条指令,AI 几乎一定会建议升级,哪怕升级没有实际收益,哪怕升级会引入兼容风险。

    这跟三个 AI 调用的温度梯度是同一思路:代码质量评估用 temperature=0.3(建议可以有创造性),Docker 策略和框架升级用 temperature=0.2(输出必须保守)。温度梯度映射的是后果严重性——糟糕的质量建议只是烦人,糟糕的 Docker 配置会导致部署失败,糟糕的升级建议会导致生产故障。后果越严重,随机性越低。

    决策四:安全维度权重只有 0.15,因为它是"弱指标"

    质量评分公式:overall = complexity × 0.25 + maintainability × 0.35 + reliability × 0.25 + security × 0.15

    为什么可维护性权重最高(0.35)?因为它是长期成本驱动因子——不可维护的代码每次修改都更难,技术债指数增长。复杂度是"当前状态"指标,可靠性是"风险"指标,可维护性是"趋势"指标,趋势预测未来成本,权重应该最高。

    但更有意思的是为什么安全权重最低(0.15)。安全评分的计算是 max(0, 100 - code_smells // 2 * 10)——把代码味道数量除以 2,当成安全问题的粗略代理。代码作者在注释里承认了这一点:"实际应该使用专门的安全分析工具"。安全评分是一个不成熟的指标 。把它加权到 0.25,这个粗略代理就会主导整体评分,产出不可靠的结果。对弱指标低权重比高权重更诚实 ——你不想让一个分母除以 2 的近似值决定一个项目能不能部署。

    安全扫描器本身是正则匹配的 12 条规则,风险评分用的是严重度几何加权(INFO=0.1, LOW=0.25, MEDIUM=0.5, HIGH=0.75, CRITICAL=1.0)加饱和曲线(10 个 CRITICAL 满分 100,再多也不涨)。几何加权防止了"修一堆 LOW 级问题来冲高分"的规避策略,饱和曲线防止了大型遗留项目因发现数量多而得分虚高。这些设计让安全扫描器本身很可靠,但安全维度的评分公式是个粗糙的代理,所以权重只能保守。

    决策五:缓存不是"读最快的层",而是"读时自动往快层迁移"

    三级缓存系统(内存 → 文件 → Redis)的标准模式是"写时写所有层,读时找最快的层"。ai-analyze 加了一个额外的行为:读时回填 。当一次读取命中 L2(文件缓存),系统把这条数据拷贝到 L1(内存)。当命中 L3(Redis),回填到 L1 和 L2。

    这意味着缓存会按使用模式自动升温 。第一次分析走 L3→L2→L1 全链路,第二次在 L2 命中后拷贝到 L1,第三次直接命中 L1。不需要预热步骤,不需要手动迁移——缓存随着使用自然地向最快层流动。

    Redis 还有一个"吸收式"容错设计:连接失败时 _client 被设为 None,之后每次操作检查 if not self._client: return None。Redis 完全挂掉不会抛异常,系统静默降级到 L1+L2 继续运行。这不是"Redis 可选"的意思——这是"Redis 的失败不应该阻塞分析流水线"。

    增量分析器还有一层额外的优化:合并缓存结果和新分析结果时,用内存里的 _file_result_cache 字典保存反序列化后的 Python 对象。MultiLevelCache 存的是序列化 JSON,合并阶段需要的是对象。反序列化是昂贵操作,在内存里保持对象状态避免了反复读缓存再反序列化的开销。这是第三层未标注的缓存,专为合并阶段存在。

    这五个决策的共同线索是:每一个"为什么不那么做"比"怎么做"更值得讲。 不暴露修改操作(架构约束),不以结构为主(可操作性优先),不让 AI 强求升级(对抗固有倾向),不给弱指标高权重(诚实优于精度),不让缓存被动等待(主动升温)。这些决策不是代码实现的细节,是设计思想的表达——它们决定了 ai-analyze 不只是一个分析工具,是一个有工程判断力的系统。

    源码导航

    模块 源码文件 说明
    MCP 协议客户端 serena_stdio_client.py 刻意不实现修改操作的安全边界
    MCP 直接调用客户端 serena_client.py 进程内全功能调用,性能优先
    统一合并层 unified_analyzer.py AST 为骨架、Serena 为补充、serena_data 空占位
    AI 集成(litellm) ai_enhanced_analyzer.py 规则先行提示词、反炒作指令、温度梯度、100+ LLM
    质量评分 quality_score.py 可维护性 0.35 最高、安全 0.15 低权重诚实代理
    安全扫描 security_scanner.py 严重度几何加权 + 饱和曲线
    三层缓存 multi_level_cache.py 读时回填自动升温、Redis 吸收式容错
    增量分析 incremental_analyzer.py MD5 变化检测 + 文件缓存渐进迁移 + 内存对象层
    AST 分析器 ast_analyzer.py 单次树遍历替代 3 次遍历、最长任务优先调度
    流水线编排 full_analyzer.py skip 标志作为成本控制网格
    Docker 生成 docker_generator.py 内容检测而非结构检测、Husky 移除实战细节
    插件系统 plugin_system.py shared_data 管道、命名空间防冲突
    异常体系 exceptions.py error_code + context dict、日志可解析格式

    项目仓库:https://github.com/erishen/ai-analyze

  • 给 AI 助手搭一个工具服务器:Lobster 的架构思路

    AI 助手只会说话,不能做事。这个问题大家都知道,OpenAI 用 Function Calling 协议给了一个解决方案——让 LLM 在对话中识别需要调用的工具,由外部服务执行。但协议只解决了一半问题:谁来提供这些工具?谁来执行?谁来保证安全?

    我给自己的 AI 助手(OpenClaw 龙虾)搭了一个工具服务器,叫 Lobster。它跑在云服务器上,龙虾通过 Function Calling 调用 Lobster 的 API,从"只会说话"变成"能办实事"。这篇文章不讲 Lobster 的具体工具清单,而是讲这套架构的思路——ToolRegistry 模式、Function Calling 桥接、安全分层、可观测性、插件扩展。这些模式你可以拿去复用,给任何 AI 助手搭工具服务器。

    第一个问题:怎么让 AI 助手"看见"你的工具

    AI 助手要调用工具,首先得知道有哪些工具可用、每个工具需要什么参数。OpenAI 定义了 Function Calling 格式——一个 JSON Schema,包含工具名、描述、参数定义。但你的工具是 Python 函数、CLI 命令、HTTP 请求,格式各不相同。怎么把它们统一转换成 AI 能理解的标准格式?

    Lobster 的做法是 ToolRegistry。核心很简单:每个工具注册时提供四样东西——名字、描述、参数定义(JSON Schema)、执行函数。注册完成后,ToolRegistry 自动把所有工具转换成 OpenAI Function Calling 格式。

    registry.register(Tool(
        name="stock_quote",
        description="获取股票实时行情",
        parameters={
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "股票代码 (如 sh600519, sz000001, hk00700)"},
            },
            "required": ["code"]
        },
        handler=lambda code: investment_tools.get_stock_quote(code),
        category="investment"
    ))
    

    注册之后,AI 助手拿工具列表就是一次 HTTP 请求:

    curl http://localhost:8000/tools/openai
    

    返回的是标准 Function Calling JSON,可以直接喂给任何支持函数调用的 LLM——GPT、Claude、DeepSeek、本地 Ollama 模型,都行。

    {
      "type": "function",
      "function": {
        "name": "stock_quote",
        "description": "获取股票实时行情",
        "parameters": {
          "type": "object",
          "properties": {
            "code": {"type": "string", "description": "股票代码 (如 sh600519, sz000001, hk00700)"},
          },
          "required": ["code"]
        }
      }
    }
    

    这个模式的好处是:你的工具实现是 Python 函数,但暴露给 AI 的格式是标准化的。加一个新工具,只需要写一个 register() 调用,不用操心格式转换。ToolRegistry 负责从注册信息到 Function Calling JSON 的映射,你只管写业务逻辑。

    第二个问题:怎么让 AI 助手"调用"你的工具

    AI 助手知道了工具列表,在对话中判断需要查股票时,它返回一个函数调用指令:

    {"name": "stock_quote", "arguments": {"code": "sh600519"}}
    

    你的服务器需要接收这个指令,找到对应的 handler,执行,返回结果。Lobster 用 FastAPI 搭了一个 API 服务器,每个注册的工具自动映射到一个执行端点:

    curl -X POST http://localhost:8000/tools/stock_quote/execute \
      -H "Content-Type: application/json" \
      -d '{"code": "sh600519"}'
    

    ToolRegistry 收到请求后做三件事:查找注册的 handler、执行、返回结果。查找是按名字索引的 O(1) 操作,执行是直接调用 Python 函数,返回是统一的 JSON 格式。

    完整的交互链路:LLM 识别意图 → 生成函数调用 → Lobster API 接收 → ToolRegistry 查找并执行 → 结果返回给 LLM → LLM 用真实数据组织回答。用户全程只跟 AI 助手对话,工具调用是透明的。

    这里有一个设计选择:你的 API 服务器也可以只暴露一个统一的 /execute 端点,不按工具名分路由。Lobster 选了按工具名分路由(/tools/{tool_name}/execute),好处是每个工具的 API 调用日志更清晰,调试时一看就知道调了哪个工具。如果你的工具数量不多,统一端点也够用。

    第三个问题:让 AI 能做事,但不能乱做事

    这是最容易被忽略的问题。你的 AI 助手能读写文件、执行命令、发 HTTP 请求了——它也就能误删文件、跑危险命令、请求内网地址。在云服务器上给 AI 开放执行能力,安全边界就是底线。

    Lobster 在每个工具执行前加了一层安全验证,分三个维度:

    路径安全。 file_readfile_write 等文件操作工具只能访问允许的目录。默认限制在当前工作目录,你可以通过配置扩展允许范围,但 AI 不能跳出边界去读 /etc/passwd 或者写 /root/.bashrc

    命令安全。 run_shell 工具有一个黑名单:rm -rf /、mkfs、dd if=、fork 炸弹、chmod 777 /、wget 这些危险命令直接拦截。你也可以配置白名单模式,只允许预设的安全命令。

    URL 安全。 http_gethttp_post 只允许 http/https 协议,并且阻断 localhost、127.0.0.1、0.0.0.0——防止 AI 被诱导去请求你服务器上的内部服务(SSRF 防护)。

    还有一个针对 Python 代码的特殊检测:run_python 工具在执行前扫描五个高危模式——os.system、subprocess、eval()、exec()、import——匹配到任何一个就拒绝执行。计算器工具用 ast.parse + operator 映射替代 eval(),只允许算术运算。

    这些验证不是装饰性的可选层,是每次工具执行的必经关卡。你的 AI 助手越能做事,你就越需要限制它能做的事。安全验证和工具能力应该同步增长——每加一个新工具,就想想它可能被怎么滥用,然后加对应的防护。

    第四个问题:怎么知道 AI 调了什么、调得怎么样

    工具服务器跑在云上,你不在旁边盯着。AI 调了哪些工具、哪个最慢、哪个失败率最高——这些信息你需要能看到。

    Lobster 做了两层可观测性:

    执行统计。 每次工具调用都记录到 ~/.lobster/stats/,持久化跨会话。统计包括:总调用次数、成功/失败次数、平均执行时间、最近调用历史。你可以看到最常用的工具排行、最慢的调用、错误率最高的工具。这些数据帮你判断哪些工具值得优化、哪些可能有问题。

    缓存。 ToolRegistry 内置了一个 LRU 缓存,带 TTL 过期机制。同一个股票代码一分钟内不重复请求新浪财经 API——因为行情数据 60 秒内变化不大,缓存命中率直接降低外部 API 调用量。缓存也有统计:命中率、缓存大小、过期清理次数。

    两层设计是互补的:统计告诉你宏观趋势(哪个工具用得多、哪个慢),缓存给你微观优化(减少重复调用、降低延迟)。如果你搭自己的工具服务器,这两层都值得做——不需要很复杂,一个 JSON 文件记统计 + 一个 OrderedDict 做缓存就够起步了。

    第五个问题:怎么加新工具

    你的 AI 助手需要的能力会不断变化。今天只需要查股票,明天可能需要读邮件,后天需要调用你公司的内部 API。工具服务器不能是封闭的——需要有一个低成本的扩展机制。

    Lobster 做了两层扩展:

    注册扩展。 加一个新工具就是一行 registry.register()。你在任何 Python 模块里写 handler 函数,然后调用注册,ToolRegistry 自动把它纳入 Function Calling 格式。不需要改 API 服务器代码,不需要手动写 JSON Schema——注册时提供的参数定义会自动转换。

    插件扩展。 ~/.lobster/plugins/*/plugin.py 自动加载。你创建一个目录,放一个 plugin.py 文件,定义你的工具,下次启动 Lobster 时自动发现和注册。有模板生成器帮你快速创建插件骨架。

    这两层的区别是:注册扩展适合你自己开发的工具(你知道代码在哪里,直接 import 就行),插件扩展适合别人贡献的工具或者你想隔离的工具(独立目录,不耦合主代码)。你的项目可能只需要注册扩展就够了,但保留插件机制意味着未来可以零成本地加第三方工具。

    关于 MCP

    你可能注意到了,Anthropic 推出了 MCP(Model Context Protocol),正在成为工具集成的新趋势。Cursor、VS Code、各种 Agent 框架都在接入 MCP。这和 Function Calling 是什么关系?

    MCP 和 Function Calling 解决的是同一个问题——让 AI 调用外部工具。但场景不同。MCP 是为 IDE 和本地工具集成设计的,AI Agent 通过 MCP 协议连接本地工具服务器,适合开发场景。Function Calling 是为云端 AI 助手设计的,LLM 在对话流中生成工具调用指令,由你的服务器执行,适合部署在云上的助手。

    Lobster 用 Function Calling 格式,因为它跑在云服务器上,龙虾也在云端。这是对的。但如果你要做 IDE 集成或本地 Agent,MCP 是更合适的协议。

    好消息是:ToolRegistry 模式和协议无关。你的工具注册、安全验证、缓存、统计这些架构层不依赖 Function Calling 的具体格式。换到 MCP,你只需要改一层——从 Function Calling JSON 转成 MCP Tool 定义格式——底下的 handler、安全验证、缓存、统计全部复用。架构思路是通用的,协议格式是可替换的。

    架构总结

    把上面五个问题串起来,Lobster 的架构是这样的:

    工具注册层(ToolRegistry)统一管理所有工具的元数据和执行函数,自动转换成 LLM 能理解的标准格式。API 服务器层(FastAPI)把每个注册工具映射到 HTTP 端点,让 AI 助手通过网络调用。安全验证层在每次执行前做路径、命令、URL 三维校验,保证 AI 能做事但不能乱做事。可观测性层记录执行统计和缓存数据,让你对 AI 的每一次工具调用有掌控。扩展层通过注册和插件两种方式加新工具,保证工具集可以持续增长。

    这五层每一层都是独立的,你可以按需取舍。如果你只需要最简方案:一个 ToolRegistry + 一个 FastAPI 服务器就够了。安全验证和缓存可以后加,插件系统可以不做。重要的是先把"AI 调用 → 工具执行 → 结果返回"这条链路跑通,然后再逐步加固。

    给 AI 助手搭工具服务器,核心不是工具本身,而是这套架构。你的工具集会变——今天是股票和代码分析,明天可能是邮件和日历。但 ToolRegistry 模式、安全分层、可观测性、扩展机制这些架构层,不管工具怎么变,都能稳稳地托住。

    源码导航

    架构层 源码文件 说明
    工具注册 tools.py ToolRegistry、14 内置工具、安全验证
    API 服务器 api_cmd.py FastAPI 端点、OpenAI Function Calling 格式
    安全验证 tools.py validate_path / validate_command / validate_url / _check_python_dangerous
    执行统计 stats.py ToolStatsTracker、持久化统计
    缓存 cache.py LRU 缓存、TTL 过期、命中率统计
    金融工具 investment.py stock_quote 等投资工具注册
    Serena 集成 serena_client.py 代码智能框架对接
    记忆存储 memory_store.py TF-IDF + Levenshtein 模糊搜索
    插件系统 plugin.py 自动发现和加载自定义插件
    配置管理 config.py .env 加载、LobsterConfig
    LLM 客户端 llm_client.py EnhancedLLMClient、对话管理、缓存、重试

    项目仓库:https://github.com/erishen/lobster

首页 文章 关于 隐私政策

© 2026 Erishen
沪ICP备2024079226号-1   沪公网安备31010502007082号