Appearance
代码分析助手
本实战构建一个能"自主探索代码库"的智能体:给它一个工作目录,它自己读文件、列目录、搜代码,最后给出结构分析、bug 提示和改进建议。核心用 create_react_agent + 自定义工具,让 LLM 在循环里自主决定下一步看哪个文件。
一、需求分析
代码分析助手的典型场景:
- 结构分析:这个项目分了哪些模块?入口在哪?
- 找 bug:这段逻辑有没有明显问题?空指针、未处理异常、资源泄漏?
- 给建议:命名、可读性、可测试性怎么改进?
人类做这件事的流程是:先看目录树了解整体 → 挑关键文件读 → 搜可疑模式 → 综合判断。让智能体模仿这个流程,关键是给它三个工具:读文件、列目录、搜代码。
二、整体架构
用 ReAct 智能体,工具是文件操作。流程由 LLM 动态决定:
mermaid
flowchart TD
U([用户问题+工作目录]) --> A[Agent LLM]
A -- 想看结构 --> T1[list_dir 列目录]
A -- 想读内容 --> T2[read_file 读文件]
A -- 想找模式 --> T3[search_code 搜代码]
T1 --> A
T2 --> A
T3 --> A
A -- 信息够了 --> R([分析报告])为什么不固定流程?因为不同代码库"该看哪"不一样——智能体读完 README 可能就知道入口,也可能要翻几个目录才找到。固定流程要么漏看、要么看一堆没用的。这正是 Agentic RAG 思想在代码场景的应用。
三、工具设计
工具是代码分析助手的核心。三个工具各司其职,关键是做好安全围栏——不能让智能体读到工作目录之外的文件。
1. 列目录工具
python
import os
from langchain_core.tools import tool
@tool
def list_dir(path: str, work_dir: str) -> str:
"""列出指定目录下的文件和子目录(仅一层)。
path: 相对于 work_dir 的路径,如 '.' 或 'src'。
work_dir: 工作目录绝对路径。
返回每行一项,目录以 '/' 结尾。"""
# 安全:把相对路径拼到 work_dir,再 resolve,防止路径越界
base = os.path.realpath(work_dir)
target = os.path.realpath(os.path.join(base, path))
if not target.startswith(base):
return "错误:路径越界,不允许访问工作目录之外的文件。"
if not os.path.isdir(target):
return f"错误:{path} 不是目录。"
items = []
for name in sorted(os.listdir(target)):
full = os.path.join(target, name)
items.append(name + "/" if os.path.isdir(full) else name)
return "\n".join(items) if items else "(空目录)"2. 读文件工具
python
@tool
def read_file(path: str, work_dir: str, max_lines: int = 200) -> str:
"""读取指定文件内容。path 是相对 work_dir 的路径。
为避免 token 爆炸,默认只读前 200 行,大文件会被截断。"""
base = os.path.realpath(work_dir)
target = os.path.realpath(os.path.join(base, path))
if not target.startswith(base):
return "错误:路径越界。"
if not os.path.isfile(target):
return f"错误:{path} 不是文件。"
try:
with open(target, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
except Exception as e:
return f"读取失败:{e}"
if len(lines) > max_lines:
head = "".join(lines[:max_lines])
return head + f"\n...(共 {len(lines)} 行,已截断到前 {max_lines} 行)"
return "".join(lines)3. 代码搜索工具
python
import re
@tool
def search_code(pattern: str, work_dir: str, file_ext: str = "") -> str:
"""在工作目录递归搜索代码,返回匹配的文件名+行号+内容。
pattern: 正则或关键字,如 'def ' 'TODO' 'eval\\('。
file_ext: 可选,按扩展名过滤,如 '.py';空表示所有文件。
最多返回 20 条匹配,避免输出过长。"""
base = os.path.realpath(work_dir)
hits = []
try:
regex = re.compile(pattern)
except re.error as e:
return f"正则错误:{e}"
for root, dirs, files in os.walk(base):
# 跳过常见无关目录
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__", "node_modules", ".venv"}]
for name in files:
if file_ext and not name.endswith(file_ext):
continue
full = os.path.join(root, name)
try:
with open(full, "r", encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f, 1):
if regex.search(line):
rel = os.path.relpath(full, base)
hits.append(f"{rel}:{i}: {line.rstrip()}")
if len(hits) >= 20:
return "\n".join(hits) + "\n...(结果过多,已截断到 20 条)"
except Exception:
continue
return "\n".join(hits) if hits else "(无匹配)"注意三个工具都做了路径越界检查(target.startswith(base))和输出截断(行数/条数上限)。这是代码分析助手最关键的安全和成本措施,下面踩坑部分会展开。
四、状态设计
create_react_agent 默认用 MessagesState。我们要额外带一个"工作目录"信息,可以用自定义状态继承 MessagesState。
python
from langgraph.graph import MessagesState
class CodeAgentState(MessagesState):
work_dir: str # 工作目录,传给工具用不过 create_react_agent 调用工具时,工具参数来自 LLM 的 tool_call,工作目录怎么传给工具?两种方案:
- 方案 A:让
work_dir作为工具参数,由 LLM 每次调用时填(如上面工具签名)。LLM 会从系统 prompt 里学到 work_dir 的值。 - 方案 B:用
RunnableConfig注入 work_dir,工具内部从 config 取,不让 LLM 看到。
方案 A 更直白、新手友好,本实战用 A。系统 prompt 里明确告诉 LLM 当前 work_dir 的绝对路径。
五、组装智能体
python
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
def build_code_agent(work_dir: str):
"""构建一个绑定到指定工作目录的代码分析智能体"""
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
system_prompt = f"""你是一个资深代码分析师,正在分析工作目录:{work_dir}
你可以调用三个工具:
- list_dir:列目录结构
- read_file:读文件内容
- search_code:搜索代码模式
工作流程建议:
1. 先用 list_dir('.') 了解整体结构
2. 根据问题挑关键文件 read_file
3. 用 search_code 找特定模式(如 'def '、'TODO'、可疑函数)
4. 综合分析后给出结论
要求:
- 结论要具体,引用文件名和行号
- 发现 bug 要说明原因和修复方向
- 不要编造没读过的文件内容
- 工作目录参数 work_dir 始终填:{work_dir}
"""
return create_react_agent(
model=llm,
tools=[list_dir, read_file, search_code],
prompt=system_prompt,
)六、准备示例代码目录
为了让示例能离线跑,我们先在本地用 Python 代码生成一个小示例项目,再让智能体去分析它。
python
import os
# 在 ./demo_project 下造一个有 bug 的小项目
demo_dir = os.path.abspath("./demo_project")
os.makedirs(demo_dir, exist_ok=True)
files = {
"README.md": "# 计算器项目\n一个简单的命令行计算器。\n",
"main.py": (
"from calc import Calculator\n\n"
"def main():\n"
" c = Calculator()\n"
" print(c.run('1 + 2 * 3'))\n\n"
'if __name__ == "__main__":\n'
" main()\n"
),
"calc.py": (
"class Calculator:\n"
" def run(self, expr):\n"
" # TODO: 现在直接 eval,有安全风险\n"
" return eval(expr)\n\n"
" def divide(self, a, b):\n"
" return a / b # 没处理除零\n"
),
"utils.py": (
"def read_file(path):\n"
" f = open(path) # 没用 with,资源泄漏\n"
" return f.read()\n"
),
}
for name, content in files.items():
with open(os.path.join(demo_dir, name), "w", encoding="utf-8") as f:
f.write(content)
print("示例项目已生成于:", demo_dir)这个示例项目故意埋了三个 bug:eval 安全风险、除零未处理、文件未关闭。看智能体能不能找出来。
七、完整可运行示例
把工具、智能体、示例项目拼起来:
python
import os
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
# ---- 工具(同上,省略重复,直接用上面定义的 list_dir/read_file/search_code)----
# 此处假定已定义好三个 @tool 函数
# ---- 造示例项目(同上)----
demo_dir = os.path.abspath("./demo_project")
# ... 生成文件的代码同上 ...
# ---- 构建智能体 ----
def build_code_agent(work_dir: str):
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
return create_react_agent(
model=llm,
tools=[list_dir, read_file, search_code],
prompt=(
f"你是资深代码分析师,工作目录:{work_dir}\n"
"工具:list_dir / read_file / search_code。\n"
"流程:先 list_dir 看结构 → read_file 关键文件 → search_code 找模式 → 综合结论。\n"
"结论要引用文件名行号,不编造。work_dir 参数始终填:" + work_dir
),
)
agent = build_code_agent(demo_dir)
if __name__ == "__main__":
question = "分析这个项目的结构,找出潜在的 bug 和改进建议。"
result = agent.invoke({"messages": [("user", question)]},
config={"recursion_limit": 25})
print("=== 最终分析 ===")
print(result["messages"][-1].content)观察智能体的探索过程
用 stream 看智能体一步步怎么探索:
python
for chunk in agent.stream(
{"messages": [("user", "找出所有可能的 bug")]},
stream_mode="updates",
config={"recursion_limit": 25},
):
for node, update in chunk.items():
print(f"\n[{node}]")
for m in update.get("messages", []):
content = str(m.content)
print(" ", type(m).__name__, ":", content[:150].replace("\n", " "))典型输出会是这样:
text
[agent]
AIMessage: 我先看看项目结构 [tool_call: list_dir({path: '.', work_dir: '...'})]
[tools]
ToolMessage: README.md
main.py
calc.py
utils.py
[agent]
AIMessage: 读 calc.py [tool_call: read_file({path: 'calc.py', ...})]
[tools]
ToolMessage: class Calculator: ...
[agent]
AIMessage: 再搜一下 eval [tool_call: search_code({pattern: 'eval', ...})]
[tools]
ToolMessage: calc.py:5: return eval(expr)
[agent]
AIMessage: 发现 3 个问题:1) calc.py:5 eval 安全风险...智能体自主完成了"列目录 → 读文件 → 搜索 → 总结"的完整探索链路。
八、常见踩坑
1. 工具安全 / 路径越界
最大风险是智能体读到工作目录之外的敏感文件(如 ~/.ssh/id_rsa、../.env)。三个工具都做了 os.path.realpath + startswith(base) 检查,这是底线。额外建议:
- 沙箱运行:把智能体跑在容器或受限用户下,即使路径检查被绕过也读不到敏感文件。
- 白名单扩展名:
read_file可加.py/.js/.ts/.md/.txt白名单,拒绝读二进制和配置密钥。 - 审计日志:每次工具调用把
path落日志,发现越界尝试立即告警。
2. token 消耗爆炸
代码分析特别费 token——一个大文件可能几千行。控制手段:
- read_file 默认截断 200 行:让智能体分多次读不同区间(可加
offset参数)。 - search_code 限制 20 条:避免一次返回几千行匹配。
- recursion_limit:设 25 左右,防止智能体陷入"读文件-再读文件"的无限探索。
- 用小模型做粗筛:列目录、搜索用便宜模型;综合分析才换大模型。
3. 长代码被截断导致分析不全
read_file 截断后,智能体可能没看到文件末尾的 bug。解决:
- 工具返回末尾明确提示"已截断,共 N 行",提示智能体可以再读后面。
- 加一个
read_file_lines(path, start, end)工具按区间读,智能体能分段读完整文件。 - 对超长文件,先用
search_code定位可疑行号,再针对性读上下文。
4. 智能体编造没读过的内容
有时智能体会"脑补"文件内容(比如它觉得 main.py 里应该有 argparse,就编出来)。抑制方法:
- 系统 prompt 强调"不要编造没读过的文件内容"。
- 让智能体在结论里必须引用文件名+行号,无法引用的结论不写。
- 关键结论让智能体先
read_file验证再下判断。
九、小结
- 代码分析助手 =
create_react_agent+ 三个工具(list_dir / read_file / search_code),让 LLM 自主探索代码库。 - 工具安全是命门:
realpath+startswith(base)防路径越界,沙箱运行兜底。 - token 控制三件套:输出截断 + recursion_limit + 大小模型分级。
- 长文件用分段读或先搜后读,避免截断导致漏分析。
- 让智能体引用文件名行号,能显著减少编造。
下一篇 深度研究 Agent 用 Send API 并行搜索多个子问题,构建 Deep Research 风格的多步研究智能体。