基于LangGraph与Ollama构建本地AI安全分析智能体:从原理到实战

发布时间:2026/8/22 11:28:26
基于LangGraph与Ollama构建本地AI安全分析智能体:从原理到实战 最近在安全圈和AI圈一个名为“Nyx”的自主红队智能体引起了不小的讨论。它由一家名为Fabraix的公司推出号称能够模拟高级持续性威胁APT攻击者的行为自动执行渗透测试任务。这听起来像是科幻电影里的情节但对于从事网络安全、特别是红队攻防演练的工程师来说这背后涉及的技术栈和实现思路远比一个新闻标题更值得深挖。本文将从一个技术实践者的角度深入剖析“自主红队智能体”这一概念。我们不会停留在新闻解读而是会拆解其可能的技术架构并尝试用开源工具链如LangGraph、Ollama构建一个本地的、简化版的AI安全分析助手原型。通过这个项目实例你将理解智能体Agent如何协调工具、处理安全数据并思考其在实际企业安全合规自动化检测中的潜在应用与边界。无论你是对AI智能体开发感兴趣的后端工程师还是希望将自动化引入安全运维的DevSecOps或是单纯想了解前沿技术动向的开发者本文都将提供从概念到代码的完整路径。1. 背景与核心概念当红队演练遇上AI智能体在深入代码之前我们必须厘清几个关键概念否则很容易陷入术语的迷雾。红队Red Team是什么在网络安全领域红队指的是一支模拟真实世界攻击者黑客的团队。他们的核心任务不是破坏而是通过授权的方式对企业网络、应用、人员进行全方位的渗透测试以发现防御体系蓝队的薄弱环节。传统的红队工作高度依赖安全专家的经验、手动工具链如Metasploit, Cobalt Strike和复杂的攻击流程编排。智能体Agent又是什么在AI语境下智能体通常指一个能够感知环境、进行决策并执行动作以达成目标的软件实体。它不同于简单的脚本其核心在于具备一定的自主性和推理能力。一个典型的AI智能体架构包括一个“大脑”通常是大型语言模型LLM或更专业的模型、一套可调用的“工具”Tools如执行命令、查询API、读写文件、一个管理其决策逻辑的“工作流”Workflow以及记忆Memory机制。那么自主红队智能体就是将两者结合构建一个AI驱动的软件实体使其能够在一定程度上自主规划并执行红队攻击链中的某些环节。例如给定一个目标如一个IP地址或域名智能体可以自动进行信息收集、漏洞扫描、尝试利用、权限提升、内网横向移动等。Nyx所宣称的正是这样一个高度自动化的系统。这带来了巨大的效率提升想象空间但也引发了关于安全边界和**“越狱”**的担忧。这里的“越狱”Jailbreak并非指iOS系统而是指在AI安全领域通过特定提示词Prompt或输入诱导AI模型突破其预设的安全和伦理限制执行危险或未经授权的操作。如果自主红队智能体被恶意利用或自身逻辑出现偏差其破坏性可能远超传统自动化工具。因此我们的技术探索必须建立在合法、授权、可控的前提下。本文接下来的实战将聚焦于构建一个用于安全合规自动化检测的辅助性智能体它将在完全受控的环境中对授权目标进行安全信息收集与风险分析旨在提高安全工程师的工作效率而非替代或模拟攻击。2. 环境准备与版本说明我们的目标是搭建一个本地运行的AI智能体原型。为了最大化可控性和学习价值我们选择全开源技术栈。核心技术选型框架LangChain / LangGraph。LangGraph是LangChain中用于构建有状态、多步骤智能体工作流的库非常适合描述红队任务中复杂的决策循环。大语言模型LLMOllama 本地模型。Ollama允许我们在本地轻松运行多种开源LLM避免API调用带来的成本和隐私问题。我们将使用较小的、性能不错的模型。编程语言Python。这是当前AI应用开发最生态的语言。工具集我们将模拟一些安全工具例如用Python封装Nmap扫描、目录爆破等实际使用中应调用真实工具。环境与版本本文示例在以下环境中测试通过但重点在于演示架构和思路版本可根据实际情况调整。操作系统Ubuntu 22.04 LTS / macOS Monterey 或更高 / Windows 11 WSL2Python3.10 或 3.11包管理pip 或 poetry关键库版本仅供参考langchain0.1.0 langchain-community0.0.10 langgraph0.0.26 ollama0.1.30 python-dotenv1.0.0 requests2.31.0项目结构预览在开始前我们先规划一下项目目录这有助于理解后续的代码组织。nyx_agent_demo/ ├── .env # 环境变量如需 ├── requirements.txt # 项目依赖 ├── main.py # 主程序入口 ├── core/ │ ├── __init__.py │ ├── agent.py # 智能体核心定义 │ ├── graph.py # LangGraph工作流定义 │ ├── state.py # 智能体状态定义 │ └── prompts.py # 系统提示词 ├── tools/ │ ├── __init__.py │ ├── base_tool.py # 工具基类 │ ├── recon_tools.py # 信息收集工具模拟 │ └── analysis_tools.py # 分析工具 └── config/ └── settings.py # 配置文件接下来我们一步步实现这个结构。3. 核心原理与技术栈拆解3.1 LangGraph智能体的“中枢神经系统”LangGraph 的核心思想是将智能体的执行过程建模为一个有向图。图中的节点Node代表一个执行步骤如调用LLM、运行工具边Edge代表步骤之间的流转条件。一个典型的红队智能体工作流可能如下开始接收任务如“对 target.com 进行Web应用安全评估”。规划节点LLM分析任务拆解为子步骤如子域名枚举 - 端口扫描 - Web目录探测 - 漏洞扫描。工具执行节点根据规划调用相应的工具如subdomain_scan工具。观察节点收集工具执行结果。决策节点LLM根据结果判断任务是否完成是否进入下一步是否需要调整计划循环重复 3-5 步直到任务完成或达到停止条件。结束汇总报告。LangGraph 帮助我们清晰地定义这个循环并管理整个过程中的状态State。3.2 智能体状态State设计状态是智能体工作流的“记忆”它随着执行步骤不断更新。对于我们的安全分析智能体状态可能需要包含# core/state.py from typing import TypedDict, List, Annotated import operator class AgentState(TypedDict): 智能体的状态定义 # 输入原始任务描述 objective: str # 输入目标如域名、IP target: str # 中间LLM生成的当前步骤计划 plan: str # 中间已执行步骤的历史记录 steps_history: List[str] # 中间工具执行产生的结果如扫描结果 tool_outputs: List[str] # 中间当前轮次工具调用的名称和参数 tool_name: str tool_args: str # 输出最终的分析报告 final_report: str # 用于LangGraph的递归状态定义简化版 GraphState TypedDict(GraphState, { messages: Annotated[list, operator.add], # LangGraph标准消息格式 objective: str, target: str, findings: list, })GraphState是LangGraph期望的格式其中messages列表的累积是管理对话历史的关键。3.3 工具Tools抽象智能体的“双手”工具是智能体与外界交互的接口。每个工具都应该有清晰的名称、描述和参数。LLM根据描述决定何时调用何工具。一个模拟的信息收集工具示例# tools/recon_tools.py from langchain.tools import BaseTool from pydantic import BaseModel, Field from typing import Type, Optional import subprocess import json class SubdomainScanInput(BaseModel): 子域名扫描工具的输入模型 domain: str Field(description要扫描的根域名例如 example.com) class SimulatedSubdomainScanTool(BaseTool): name subdomain_scan description 对一个域名进行子域名枚举。输入应为根域名。 args_schema: Type[BaseModel] SubdomainScanInput return_direct: bool False # 工具输出是否直接作为最终结果 def _run(self, domain: str) - str: 模拟子域名扫描。 在实际应用中这里应集成 amass, subfinder, assetfinder 等真实工具。 此处返回模拟数据用于演示。 # 模拟调用外部命令的过程 print(f[*] 正在对 {domain} 进行子域名扫描模拟...) # 假设的扫描结果 simulated_results [ fwww.{domain}, fmail.{domain}, fapi.{domain}, fdev.{domain}, fadmin.{domain}, ] result_str f目标 {domain} 的子域名枚举完成发现 {len(simulated_results)} 个子域名\n \n.join(simulated_results) return result_str async def _arun(self, domain: str): 异步版本可选 raise NotImplementedError(此工具暂不支持异步调用) # 类似地可以定义 PortScanTool, DirectoryBruteTool, VulnCheckTool 等。3.4 提示词Prompt工程引导智能体的“思维”提示词是指导LLM行为的关键。一个好的系统提示词需要明确角色、任务边界、可用工具和输出格式。# core/prompts.py SYSTEM_PROMPT 你是一个专业的安全分析助手白帽子。你的任务是在获得明确授权的前提下对指定目标进行安全信息收集和初步风险分析并生成一份简洁的报告。 你必须严格遵守以下规则 1. 你只能使用我提供给你的工具。 2. 你的所有操作都必须基于给定的目标且仅用于安全评估目的。 3. 在报告中你需要清晰列出发现的信息如子域名、开放端口并对潜在风险进行分级高、中、低说明。 4. 报告最后应给出后续人工深入检查的建议。 请按步骤思考并决定下一步该使用哪个工具。当你认为已收集到足够信息或无法继续时请生成最终报告。 可用工具 {tools_descriptions} 当前任务{objective} 评估目标{target} 开始你的工作。首先请制定一个初步的评估计划。 这个提示词定义了智能体的角色安全分析助手、约束授权、工具限制和输出要求报告格式。4. 完整实战构建本地AI安全分析智能体现在我们将所有部分组合起来创建一个可运行的智能体。4.1 创建项目并安装依赖首先创建项目目录并初始化虚拟环境。mkdir nyx_agent_demo cd nyx_agent_demo python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate创建requirements.txt文件langchain0.1.0 langchain-community0.0.10 langgraph0.0.26 ollama0.1.30 python-dotenv1.0.0 requests2.31.0 pydantic2.0.0安装依赖pip install -r requirements.txt4.2 准备本地大语言模型Ollama我们需要一个本地运行的LLM作为智能体的“大脑”。这里使用Ollama来运行llama3.2:3b模型它体积较小适合本地实验。安装Ollama访问 Ollama官网 下载并安装。拉取模型在终端运行ollama pull llama3.2:3b。验证运行运行ollama run llama3.2:3b输入简单问题测试是否正常。4.3 编写核心代码第一步定义工具集在tools/__init__.py中导出工具。# tools/__init__.py from .recon_tools import SimulatedSubdomainScanTool, SimulatedPortScanTool from .analysis_tools import SimulatedVulnerabilityAnalyzerTool __all__ [ SimulatedSubdomainScanTool, SimulatedPortScanTool, SimulatedVulnerabilityAnalyzerTool, ]我们还需要补充另外两个模拟工具代码类似仅展示PortScanTool的关键部分# tools/recon_tools.py (追加) class SimulatedPortScanTool(BaseTool): name port_scan description 对一个IP地址或主机名进行常用端口扫描。输入应为主机地址。 # ... 省略 args_schema 定义 def _run(self, host: str) - str: print(f[*] 正在对 {host} 进行端口扫描模拟...) simulated_ports [ 22/tcp open ssh, 80/tcp open http, 443/tcp open https, 3306/tcp open mysql, ] return f主机 {host} 的端口扫描结果\n \n.join(simulated_ports)第二步构建智能体工作流Graph这是最核心的部分在core/graph.py中实现。# core/graph.py from typing import TypedDict, Annotated, Sequence import operator from langgraph.graph import StateGraph, END from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage from langchain_community.chat_models import ChatOllama from core.prompts import SYSTEM_PROMPT from tools import SimulatedSubdomainScanTool, SimulatedPortScanTool, SimulatedVulnerabilityAnalyzerTool import json # 1. 定义图的状态 class GraphState(TypedDict): messages: Annotated[list, operator.add] objective: str target: str findings: Annotated[list, operator.add] # 2. 初始化模型和工具 llm ChatOllama(modelllama3.2:3b, temperature0.1) tools [SimulatedSubdomainScanTool(), SimulatedPortScanTool(), SimulatedVulnerabilityAnalyzerTool()] llm_with_tools llm.bind_tools(tools) # 3. 定义节点函数 def agent_node(state: GraphState): 智能体节点调用LLM决定下一步行动说话或调用工具。 messages state[messages] # 如果是第一次调用添加系统提示词 if len(messages) 1 and isinstance(messages[0], HumanMessage): system_message SystemMessage(contentSYSTEM_PROMPT.format( tools_descriptions\n.join([f- {tool.name}: {tool.description} for tool in tools]), objectivestate[objective], targetstate[target] )) messages [system_message] messages # 调用LLM response llm_with_tools.invoke(messages) # 将LLM的响应添加到消息历史 return {messages: [response]} def tool_node(state: GraphState): 工具节点执行LLM选择的工具。 messages state[messages] last_message messages[-1] tool_calls last_message.tool_calls if hasattr(last_message, tool_calls) else [] tool_messages [] for tool_call in tool_calls: tool_name tool_call[name] tool_args tool_call[args] # 查找对应的工具实例 tool_to_use next((tool for tool in tools if tool.name tool_name), None) if tool_to_use: print(f[执行工具] {tool_name} with args: {tool_args}) try: # 同步执行工具 tool_output tool_to_use._run(**tool_args) # 将工具输出作为 ToolMessage 添加 tool_messages.append(ToolMessage(contenttool_output, tool_call_idtool_call[id])) # 将发现物存入状态简化处理这里只存文本 state[findings].append(f{tool_name}: {tool_output[:100]}...) except Exception as e: error_msg f工具 {tool_name} 执行出错: {str(e)} tool_messages.append(ToolMessage(contenterror_msg, tool_call_idtool_call[id])) else: error_msg f错误未找到名为 {tool_name} 的工具。 tool_messages.append(ToolMessage(contenterror_msg, tool_call_idtool_call[id])) return {messages: tool_messages} # 4. 定义条件边函数 def should_continue(state: GraphState) - str: 根据最后一条消息决定下一步是调用工具还是结束。 messages state[messages] last_message messages[-1] # 如果LLM返回了工具调用则进入工具节点 if hasattr(last_message, tool_calls) and last_message.tool_calls: return use_tool # 否则认为任务结束LLM生成了最终回答 else: return end # 5. 构建图 def create_workflow(): workflow StateGraph(GraphState) # 添加节点 workflow.add_node(agent, agent_node) workflow.add_node(tool, tool_node) # 设置入口点 workflow.set_entry_point(agent) # 添加条件边 workflow.add_conditional_edges( agent, should_continue, { use_tool: tool, end: END, } ) # 从工具节点无条件返回智能体节点 workflow.add_edge(tool, agent) # 编译图 return workflow.compile() # 导出创建好的工作流 graph create_workflow()第三步创建主程序入口在main.py中我们初始化状态并运行图。# main.py from core.graph import graph from langchain_core.messages import HumanMessage def main(): print( 本地AI安全分析智能体演示 \n) # 用户输入任务和目标这里写死作为示例 objective 进行Web应用安全信息收集 target test.example.com # 请替换为你有权测试的域名或IP print(f任务: {objective}) print(f目标: {target}) print(\n智能体开始工作...\n) # 初始化图状态 initial_state { messages: [HumanMessage(contentf请对目标 {target} 执行安全评估。)], objective: objective, target: target, findings: [] } # 运行图 final_state None for step, output in enumerate(graph.stream(initial_state), start1): print(f\n--- 步骤 {step} ---) for key, value in output.items(): if key agent or key tool: last_msg value[messages][-1] if hasattr(last_msg, content): print(f[{key.upper()}] 输出: {last_msg.content[:200]}...) elif hasattr(last_msg, tool_calls): print(f[{key.upper()}] 决定调用工具: {last_msg.tool_calls}) # 获取最终状态 final_state graph.get_state(graph.get_state().config) print(\n *50) print(任务执行完毕) print(\n收集到的发现) for i, finding in enumerate(final_state.values[findings], 1): print(f{i}. {finding}) # 提取最终的LLM回答作为报告 final_messages final_state.values[messages] final_ai_message None for msg in reversed(final_messages): if isinstance(msg, AIMessage) and not hasattr(msg, tool_calls): final_ai_message msg break if final_ai_message: print(\n最终分析报告) print(final_ai_message.content) else: print(\n未生成最终报告。) if __name__ __main__: main()4.4 运行与验证在项目根目录下运行python main.py你应该能看到类似以下的输出具体内容因模型随机性略有不同 本地AI安全分析智能体演示 任务: 进行Web应用安全信息收集 目标: test.example.com 智能体开始工作... --- 步骤 1 --- [AGENT] 输出: 我将对 test.example.com 进行安全评估。首先我需要收集信息。计划如下1. 子域名枚举2. 端口扫描3. 漏洞分析。现在开始第一步使用 subdomain_scan 工具... --- 步骤 2 --- [TOOL] 输出: [*] 正在对 test.example.com 进行子域名扫描模拟... [AGENT] 决定调用工具: [{name: subdomain_scan, args: {domain: test.example.com}, id: ...}] --- 步骤 3 --- [AGENT] 输出: 子域名扫描完成。发现 www.test.example.com, mail.test.example.com 等。接下来进行端口扫描... --- 步骤 4 --- [TOOL] 输出: [*] 正在对 test.example.com 进行端口扫描模拟... [AGENT] 决定调用工具: [{name: port_scan, args: {host: test.example.com}, id: ...}] --- 步骤 5 --- [AGENT] 输出: 端口扫描完成。发现 22, 80, 443 端口开放。现在进行漏洞分析... --- 步骤 6 --- [TOOL] 输出: [*] 正在分析 test.example.com 的潜在漏洞模拟... [AGENT] 决定调用工具: [{name: vuln_analyzer, args: {target: test.example.com}, id: ...}] --- 步骤 7 --- [AGENT] 输出: 所有信息收集步骤已完成。现在生成最终报告... 任务执行完毕 收集到的发现 1. subdomain_scan: 目标 test.example.com 的子域名枚举完成发现 5 个子域名... 2. port_scan: 主机 test.example.com 的端口扫描结果... 3. vuln_analyzer: 对 test.example.com 的漏洞分析完成... 最终分析报告 **安全评估报告 - test.example.com** **1. 信息收集摘要** - **子域名**共发现5个包括www, mail, api等。 - **开放端口**22(SSH), 80(HTTP), 443(HTTPS), 3306(MySQL)。 **2. 潜在风险分析** - **高风险**发现MySQL数据库端口3306对公网开放可能存在弱口令或未授权访问风险。 - **中风险**SSH端口22开放建议检查密钥认证强度与失败登录策略。 - **低风险**HTTP/HTTPS服务运行正常未发现明显配置错误。 **3. 后续建议** 1. 立即检查MySQL服务访问控制禁止公网IP访问或设置强密码。 2. 对SSH服务进行加固如禁用root登录、使用密钥认证。 3. 对Web应用80/443进行深入的漏洞扫描如使用Nessus, Acunetix。 4. 对所有发现的子域名进行同样的安全检查。 **评估结论**目标存在需要立即关注的中高风险项建议优先处理数据库和SSH服务的暴露问题。4.5 结果说明我们成功运行了一个本地AI安全分析智能体原型。它能够理解任务接收自然语言描述的安全评估任务。制定计划自动拆解任务为“子域名扫描-端口扫描-漏洞分析”的步骤。调用工具按计划顺序调用我们定义的模拟工具。分析决策根据工具返回的结果决定下一步行动继续调用工具或生成报告。生成报告汇总发现并给出结构化的风险分析和建议。这个原型虽然工具是模拟的但完整演示了自主智能体在安全领域的核心工作流程感知接收任务- 规划拆解步骤- 行动调用工具- 观察获取结果- 循环直至完成。5. 常见问题与排查思路在构建和运行此类智能体时你可能会遇到以下问题问题现象常见原因解决思路Ollama 模型无法连接Ollama服务未启动模型未下载端口被占用。1. 运行ollama serve启动服务。2. 运行ollama list确认模型存在。3. 检查ChatOllama初始化时指定的模型名称是否正确。智能体陷入死循环工具描述不清晰LLM无法理解何时停止状态判断逻辑有误。1. 优化工具的描述使其职责单一明确。2. 在系统提示词中强化停止条件如“当你认为信息足够时生成报告”。3. 在should_continue函数中添加最大步数限制。工具调用参数错误Pydantic输入模型定义与工具_run方法参数不匹配LLM生成参数格式错误。1. 确保args_schema中字段的description清晰引导LLM生成正确格式。2. 在工具的_run方法开头添加参数验证和日志。3. 使用try-except捕获异常并返回友好错误信息给LLM。LangGraph 状态更新异常对Annotated[list, operator.add]的理解有误节点返回值格式不对。1. 理解operator.add意味着该字段的值会在节点间追加而非替换。2. 确保每个节点函数返回的字典键与GraphState定义一致。3. 使用print或日志仔细检查每个节点输入/输出的状态。最终报告内容空洞LLM在最终步骤没有得到足够的上下文历史消息过长被截断。1. 确保最终生成报告的节点能访问到关键的findings状态。2. 在提示词中明确要求报告需引用具体的发现项。3. 可以考虑在状态中专门维护一个report_sections列表逐步填充。6. 最佳实践与工程建议将AI智能体应用于安全领域尤其是向“自主”方向发展时必须建立严格的安全与工程规范。6.1 安全与合规第一明确授权智能体只能在明确授权的范围和目标内运行。在代码中硬编码授权检查逻辑或通过启动参数强制传入授权凭证/令牌。操作审计记录智能体的每一个决策、调用的每一个工具及其参数和结果。这些日志是事后审查、问题排查和合规证明的关键。“断路器”机制必须设计人工干预接口。例如监控一个外部文件或API一旦接收到停止信号智能体应立即终止所有活动。避免智能体在失控状态下持续运行。工具权限隔离为智能体分配最小必要权限。例如运行扫描的容器或账户不应具有修改系统配置或访问核心数据的权限。6.2 工程化与可维护性配置化管理将模型端点、工具开关、超时时间、重试策略等抽取到配置文件如config/settings.py或config.yaml中。工具标准化所有工具应遵循统一的接口如继承自BaseTool并包含完善的错误处理和日志记录。考虑为工具执行添加超时和资源限制。模块化设计如本文所示将状态、图、工具、提示词分离。这使得替换LLM如从Ollama切换到OpenAI API、增加新工具或调整工作流变得非常容易。测试与验证为每个工具编写单元测试。为关键的工作流路径如“规划-扫描-报告”编写集成测试使用固定的模拟输入验证输出是否符合预期。6.3 性能与可靠性LLM调用优化本地小模型响应快但能力有限云端大模型能力强但延迟高、有成本。根据任务复杂度做混合调度。对于简单的、模式固定的任务如解析扫描结果可以尝试用更轻量的规则引擎或小模型。异步执行如果工具调用是I/O密集型如网络请求应使用异步版本_arun以提高整体吞吐量。LangGraph支持异步节点。状态管理对于长周期任务需要将状态持久化如存入数据库以便在程序重启后能恢复。GraphState需要能被序列化和反序列化。兜底策略智能体可能“卡住”或做出无意义决策。需要设置最大迭代次数、超时时间并在最终节点强制生成一个包含已收集信息的“不完全报告”。6.4 从原型到实用系统的思考本文的演示是一个高度简化的原型。一个实用的企业级系统还需要考虑真实工具集成将模拟工具替换为真正的Nmap、SQLmap、 nuclei、各种云服务API的封装。复杂工作流红队活动不是线性流程而是根据结果动态调整的树状或图状流程。LangGraph的Conditional Edges和State可以描述这种复杂性。多智能体协作可以设计多个 specialized agent如一个负责侦察一个负责漏洞利用一个负责报告生成通过一个“主管”智能体Supervisor Agent进行协调。这正是多智能体系统MAS的研究方向。知识库与记忆让智能体能够从历史任务中学习避免重复错误或应用之前成功的策略。构建自主红队智能体是一条充满挑战但前景广阔的道路。它不仅是工具的自动化更是安全专家经验和决策过程的编码化与增强。作为开发者我们既要积极拥抱这项技术带来的效率革命也必须以最高的审慎态度来构建它的安全边界与伦理护栏。从今天这个简单的本地原型出发你可以逐步探索更强大的模型、更真实的工具链和更复杂的协作模式但请永远将“可控”与“负责”置于技术追求之上。