import json
import os
from agents import function_tool
from firecrawl import firecrawl
def_get_firecrawl_client():
api_key = os.getenv("FIRECRAWL_API_KEY")ifnot api_key:raise RuntimeError("FIRECRAWL_API_KEY is not set")return firecrawl(api_key=api_key)技术支持
@function_tooldeffirecrawl_search(query:str, limit:int=3)->str:"""
Run a Firecrawl search for docs related to the issue or tech stack.
Args:
query: Search query (usually based on issue title / framework / error message).
limit: Max number of results to return.
Returns:
JSON string of Firecrawl search results.
"""
client = _get_firecrawl_client()
results = client.search(query=query, limit=limit)return json.dumps(results)技术支持
@function_tooldeffirecrawl_scrape(url:str)->str:"""
Scrape a single URL using Firecrawl for deeper research.
Args:
url: URL to scrape (docs, blog, README in another repo, etc.).
Returns:
JSON (markdown/structured) content from Firecrawl scrape.
"""
client = _get_firecrawl_client()
result = client.scrape(url=url, params={"formats":["markdown"]})return json.dumps(result)技术支持
GitHub CLI 工具
然后,在 src/tools 目录下创建 github_tools.py 文件,并加入以下代码。
1. 通过 GitHub CLI 获取指定 Issue,并以 JSON 返回其详情,供代理阅读。
import base64
import json
import subprocess
from typing import List, Optional
from agents import function_tool
@function_tooldefget_github_issue(repo:str, issue_number:int)->str:"""
Fetch a GitHub issue using the GitHub CLI.
Args:
repo: Repository in 'owner/name' format.
issue_number: The issue number to fetch.
Returns:
A JSON string containing the issue fields (title, body, labels, URL, etc.),
or an error payload if the command fails.
"""try:
result = subprocess.run(["gh","issue","view",str(issue_number),"--repo",
repo,"--json","number,title,body,labels,url,author,createdAt,state,assignees",],
capture_output=True,
text=True,
check=True,)return result.stdout
except subprocess.CalledProcessError as e:return json.dumps({"error":"Failed to fetch issue via GitHub CLI","stderr": e.stderr,"repo": repo,"issue_number": issue_number,})技术支持
2. 仅列出远程仓库中相关的文件(可按路径与扩展名过滤),避免代理扫描整个项目。
@function_tooldeflist_repo_files_gh(
repo:str,
max_files:int=80,
extensions: Optional[List[str]]=None,
path_prefixes: Optional[List[str]]=None,)->str:"""
List *relevant* files in the remote repo using GitHub CLI.
Uses:
gh api repos/{repo}/git/trees/HEAD?recursive=1
The agent is expected to reason first which areas of the codebase are likely relevant
(e.g. 'src/', 'app/', 'backend/api/', 'cli/'), and then call this tool with a small
set of path_prefixes instead of scanning the entire project.
Args:
repo: Repository in 'owner/name' format (e.g. openai/openai-agents-python).
max_files: Max number of files to return.
extensions: Optional list of file extensions to keep (e.g. [".py", ".ts"]).
path_prefixes: Optional list of path prefixes to include (e.g. ["src/", "app/api/"]).
Returns:
JSON string with file paths and filters applied.
"""try:
result = subprocess.run(["gh","api",f"repos/{repo}/git/trees/HEAD?recursive=1",],
capture_output=True,
text=True,
check=True,)except subprocess.CalledProcessError as e:return json.dumps({"error":"Failed to list repo files via GitHub CLI","stderr": e.stderr,"repo": repo,})try:
data = json.loads(result.stdout)except json.JSONDecodeError:return json.dumps({"error":"Failed to parse JSON from gh api","raw": result.stdout[:2000],"repo": repo,})
tree = data.get("tree",[])if extensions isnotNoneandnotisinstance(extensions,list):
extensions =[str(extensions)]
exts =[e.lower()for e in(extensions or[])]if path_prefixes isnotNoneandnotisinstance(path_prefixes,list):
path_prefixes =[str(path_prefixes)]
prefixes =[p.strip()for p in(path_prefixes or[])if p.strip()]
paths: List[str]=[]for entry in tree:if entry.get("type")!="blob":continue# only files
path = entry.get("path","")ifnot path:continue# If prefixes are provided, only keep files under those subtreesif prefixes andnotany(path.startswith(pref)for pref in prefixes):continueif exts:
suffix ="."+ path.split(".")[-1].lower()if"."in path else""if suffix notin exts:continue
paths.append(path)iflen(paths)>= max_files:breakreturn json.dumps({"repo": repo,"count":len(paths),"files": paths,"filtered_by_extensions":bool(exts),"filtered_by_prefixes":bool(prefixes),})技术支持
@function_tooldefget_repo_file_gh(
repo:str,
path:str,
ref:str="",
max_chars:int=8000,)->str:"""
Read a file's contents from the remote repo using GitHub CLI.
Uses:
gh api repos/{repo}/contents/{path} [ -F ref=<branch> ]
Args:
repo: Repository in 'owner/name' format.
path: File path in the repo (e.g. 'src/main.py').
ref: Optional branch / commit / tag ref (default: repo's default branch).
max_chars: Max characters of decoded content to return.
Returns:
JSON with file metadata and decoded content (truncated if needed),
or an error payload if anything fails.
"""
cmd =["gh","api",f"repos/{repo}/contents/{path}"]# Only add ref when explicitly set (GitHub default branch otherwise)if ref:
cmd +=["-F",f"ref={ref}"]try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,)except subprocess.CalledProcessError as e:return json.dumps({"error":"Failed to fetch file via GitHub CLI","stderr": e.stderr,"repo": repo,"path": path,"ref": ref or"DEFAULT_BRANCH",})try:
data = json.loads(result.stdout)except json.JSONDecodeError:return json.dumps({"error":"Failed to parse JSON from gh api (contents)","raw": result.stdout[:2000],"repo": repo,"path": path,})if data.get("type")!="file":return json.dumps({"error":"Path is not a file","repo": repo,"path": path,"data_type": data.get("type"),})
encoding = data.get("encoding")
content_b64 = data.get("content","")if encoding !="base64":return json.dumps({"error":"Unexpected encoding","repo": repo,"path": path,"encoding": encoding,})try:# GitHub often includes newlines in base64 payload
raw_bytes = base64.b64decode(content_b64)
text = raw_bytes.decode("utf-8", errors="replace")except Exception as e:# noqa: BLE001return json.dumps({"error":f"Failed to decode file content: {e}","repo": repo,"path": path,"encoding": encoding,})
truncated = text[:max_chars]return json.dumps({"repo": repo,"path": path,"ref": ref or"DEFAULT_BRANCH","truncated":len(text)> max_chars,"content": truncated,})技术支持
from agents import Agent
from tools.github_tools import(
get_github_issue,
list_repo_files_gh,
get_repo_file_gh,)from tools.firecrawl_tools import(
firecrawl_search,
firecrawl_scrape,)defbuild_planner_agent()-> Agent:"""
Issue Planner agent that:
- Reads the GitHub issue
- Reasons about which parts of the repo are relevant
- Uses GitHub CLI to inspect a *small* set of files online
- Uses Firecrawl for external research
- Outputs a concrete, step-by-step execution plan
"""return Agent(
name="Issue Planner",
instructions=("You are a senior software engineer.\n""Goal: Given a GitHub issue and the online repo (structure + files), plus optional ""external research, produce a clear, step-by-step execution plan to resolve the issue.\n\n""CONTEXT:\n""- All repository interaction must be done *online* via GitHub CLI tools.\n""- You have tools to: read the issue, list files under certain paths, read specific files, "" and call Firecrawl search/scrape for docs.\n\n""IMPORTANT STRATEGY (BE SMART):\n""- Be selective and cost-aware. Do NOT scan the whole project.\n""- First, deeply read the issue and infer which part of the system it affects:\n"" routing layer, CLI, API handlers, DB layer, tests, etc.\n""- Based on this reasoning, decide a small list of path prefixes and file types.\n\n""RECOMMENDED WORKFLOW:\n""1. Call get_github_issue(repo, issue_number) to fully understand the problem.\n""2. From the issue, infer a small list of path prefixes where relevant code likely lives,\n"" e.g. ['src/', 'app/', 'backend/api/', 'cli/'] depending on the project style.\n""3. Call list_repo_files_gh with:\n"" - extensions like ['.py', '.ts', '.js', '.tsx', '.jsx']\n"" - path_prefixes set to that small, targeted list\n"" This keeps the search focused instead of scanning the entire project.\n""4. From the returned file list, pick at most ~5-15 key files that are most likely related\n"" (entrypoints, routers, handlers, services, tests).\n""5. Call get_repo_file_gh(repo, path=...) only on those selected files to inspect the "" actual implementation.\n""6. If you need framework or library context (FastAPI, Click, React, etc.), use\n"" firecrawl_search and firecrawl_scrape to pull official docs or good examples.\n\n""OUTPUT FORMAT (execution plan):\n""After you have enough context from the issue + targeted code inspection (+ optional research), ""output a concise but concrete plan with sections:\n"" - Issue summary\n"" - Project/codebase understanding (where this issue lives in the architecture)\n"" - Key files / components to touch (with file paths)\n"" - Step-by-step implementation plan (Step 1, Step 2, ...)\n"" - Testing strategy (unit / integration / manual)\n"" - Edge cases, risks, and any open questions\n\n""The plan must be actionable for a mid-level developer. Avoid generic advice; tie your steps ""to the actual files and modules you inspected.\n"),
tools=[
get_github_issue,
list_repo_files_gh,
get_repo_file_gh,
firecrawl_search,
firecrawl_scrape,],
model="gpt-5.1-codex",)技术支持
1. 先完成 CLI 所需的一切导入与设置。包含标准库、在 Windows 上确保正确的 Unicode 输出(正确显示表情与符号),并导入 OpenAI 的运行器以及我们的规划代理。
2. 接着定义命令行选项,允许您在运行工具时传入仓库与 Issue 编号。
src/app.py:
import argparse
import asyncio
import json
import os
import pathlib
import sys
from datetime import datetime
# Set UTF-8 encoding for stdout to handle Unicode charactersif sys.platform =="win32":import codecs
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach())from agents import Runner, ItemHelpers
from openai.types.responses import ResponseTextDeltaEvent
from agents_pkg.planner_agent import build_planner_agent
defparse_args()-> argparse.Namespace:
parser = argparse.ArgumentParser(
description=("Issue Planner: GPT-5.1-Codex + OpenAI Agents + GitHub CLI + Firecrawl\n"))
parser.add_argument("--repo",help="GitHub repo in 'owner/name' format (e.g. openai/openai-agents-python).",)
parser.add_argument("--issue",type=int,help="Issue number to plan for.",)return parser.parse_args()技术支持
3. 然后收集输入并准备上下文。读取参数中的仓库与 Issue,确保已设置 OpenAI API Key,构建清晰的提示词,指导代理按步骤分析 Issue,并创建带时间戳的 markdown 文件用于保存最终计划。
defget_user_input(args: argparse.Namespace)->tuple[str,int]:"""Get repository and issue number from arguments or user input."""
repo = args.repo orinput("GitHub repo (owner/name): ").strip()
issue_number = args.issue orint(input("Issue number: ").strip())return repo, issue_number
defvalidate_environment()->None:"""Validate that required environment variables are set."""ifnot os.getenv("OPENAI_API_KEY"):raise RuntimeError("OPENAI_API_KEY is not set")defbuild_user_prompt(repo:str, issue_number:int)->str:"""Build the user prompt for the agent."""return(f"You are helping me plan how to implement GitHub issue #{issue_number} "f"in repo '{repo}'.\n\n""Be selective and cost-aware:\n""1. Use get_github_issue(repo, issue_number) to understand the problem.\n""2. Based on the issue text, first reason about which directories and components "" are likely relevant.\n""3. Call list_repo_files_gh(repo, extensions=['.py', '.ts', '.js', '.tsx', '.jsx'], "" path_prefixes=[<your inferred prefixes>]) to only explore those areas.\n""4. From those results, choose a small set of the most relevant files and call "" get_repo_file_gh(repo, path=...) on them.\n""5. Optionally, use firecrawl_search and firecrawl_scrape if you need external docs.\n""6. Finally, generate the execution plan in the structured format from your instructions.")defsetup_output_file(repo:str, issue_number:int)-> pathlib.Path:"""Create output directory and return the markdown file path."""
output_dir = pathlib.Path("output")
output_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
markdown_file = output_dir /f"execution_plan_{repo.replace('/','_')}_issue_{issue_number}_{timestamp}.md"return markdown_file技术支持
defextract_reasoning_text(event_item)->str|None:"""Extract reasoning text from a reasoning event item."""
reasoning_text =Noneifhasattr(event_item,'raw_item'):
raw = event_item.raw_item
# Try multiple attribute namesfor attr_name in['content','text','reasoning','message','delta']:ifhasattr(raw, attr_name):
val =getattr(raw, attr_name)if val andstr(val).strip()andstr(val)!='None':
reasoning_text =str(val)break# If still not found, try to access as dict-likeifnot reasoning_text:try:ifhasattr(raw,'__dict__'):for key, val in raw.__dict__.items():if val andstr(val).strip()andstr(val)!='None'and key in['content','text','reasoning','message','delta']:
reasoning_text =str(val)breakexcept:pass# Also try direct attributes on event.itemifnot reasoning_text:for attr_name in['content','text','reasoning','message']:ifhasattr(event_item, attr_name):
val =getattr(event_item, attr_name)if val andstr(val).strip()andstr(val)!='None':
reasoning_text =str(val)breakreturn reasoning_text
defhandle_reasoning_event(event_item)->None:"""Handle and display reasoning events."""
reasoning_text = extract_reasoning_text(event_item)if reasoning_text and reasoning_text.strip():# Show first line or first 100 chars
first_line = reasoning_text.split('\n')[0].strip()[:100]iflen(reasoning_text.split('\n')[0].strip())>100:
first_line +="..."print(f"\n💭 Reasoning: {first_line}", flush=True)else:# Don't show "None" - just show that reasoning is happeningprint(f"\n💭 Reasoning...", flush=True)技术支持
OpenAI 已低调在开发者平台发布 GPT-5.1-Codex,开发者纷纷称其为当下最强的代码模型。与早期 Codex 版本不同,GPT-5.1-Codex 专为真实的软件工程、长时推理与工具型代理而打造。
在本教程中,我们将使用 OpenAI Agents 与 GPT-5.1 Codex 构建一个完整的 GitHub Issue 分析代理(GitHub Issue Analyzer Agent)。
该代理将能够:
这个代理的行为像一位资深工程师:在动手前会先调研、阅读、推理并规划。您也可以查看我们关于 GPT-5.1 的指南,了解更多更新内容。
什么是 GPT-5.1-Codex?
GPT-5.1 Codex 是 GPT-5.1 的一个专业化版本,面向长时运行的代理型编码任务,而不仅仅是代码片段补全。它为真实的软件工程与代理式工作流而生,正是我们将 Issue 自动转化为计划这一流程的理想引擎。
与通用模型不同,Codex 能像资深工程师一样理解代码库;它会阅读 Issue、基于架构进行推理、定位正确的目录,并只检查真正重要的文件。这使代理更快、更聪明且更具成本效率。
Codex 针对长时运行的代理型编码任务进行了优化。它可自然集成开发者工具,如 GitHub CLI 与 Firecrawl API,使我们的代理能够拉取 Issue、在线探索项目结构、读取特定文件,并按需收集文档。它能严格遵循指令,产出干净可靠的分析,并自适应推理力度:简单任务快速推进,复杂任务深入分析。
通过将强大的代码理解能力与工具感知的推理相结合,GPT-5.1 Codex 让我们的代理能够把一个 GitHub Issue 转化为准确、可执行的工程计划,而无需扫描整个仓库或幻想编造代码。它为整个工作流提供“工程直觉、结构与精确度”的支撑,是项目的中枢。
搭建 GitHub Issue 分析器
在开始之前,请先准备您的环境。您需要在机器上安装 Git。如果不确定,请运行
git --version进行确认。您还需要一个 OpenAI 开发者平台账户,至少有 $6 额度,以保证 API 调用顺畅。接着,注册免费的 Firecrawl 账号,并将 API Key 设置为环境变量。它们用于让分析器与 OpenAI 和 Firecrawl 通信:
完成后,安装支撑该工作流的 Python 包。第一个 openai-agents 是一个轻量框架,让构建多代理流水线变得异常简单。第二个 firecrawl-py 负责爬取并从您的仓库或文档中抽取有用信息。
最后,请确保已安装并配置 GitHub CLI。以下命令可帮助您登录 GitHub 账户。
使用 GPT-5.1-Codex 构建 GitHub Issue 分析代理
我们将创建一个名为 "src" 的文件夹,包含所有代码文件。其中 "agents_pkg" 文件夹用于存放代理相关文件,"tools" 文件夹用于存放工具相关文件。
主应用程序 "app.py" 提供命令行界面(CLI),利用规划代理根据用户输入生成 GitHub Issue 报告。
项目目录结构如下:
1. Web 与 GitHub 工具
首先,我们将创建一个工具文件,通过简单的 Python 函数帮助代理访问 GitHub 与 Firecrawl API。
Firecrawl API 工具
先从工具开始。在 src/tools 目录下创建 firecrawl_tools.py 文件,并加入以下代码。
1. 使用环境变量中的
FIRECRAWL_API_KEY创建并返回 Firecrawl 客户端;若缺失将抛出错误。2. 使用 Firecrawl 进行聚焦的网页检索(如文档、博客、错误信息),并以 JSON 返回结果,供代理作为外部上下文使用。
3. 使用 Firecrawl 抓取单个 URL(markdown 格式),并将结构化页面内容以 JSON 返回,便于深入技术调研。
GitHub CLI 工具
然后,在 src/tools 目录下创建 github_tools.py 文件,并加入以下代码。
1. 通过 GitHub CLI 获取指定 Issue,并以 JSON 返回其详情,供代理阅读。
2. 仅列出远程仓库中相关的文件(可按路径与扩展名过滤),避免代理扫描整个项目。
3. 使用 GitHub CLI 下载并解码仓库中单个文件内容,将文本(必要时截断)以 JSON 返回。
2. 规划代理
这里我们定义一个 Issue Planner(问题规划) 代理,它会:
我们接入 GitHub 与 Firecrawl 工具,给代理详细的节省成本的工作指引,并指定运行在
gpt-5.1-codex模型上。在
src/agents_pkg目录中创建planner_agent.py文件,并加入以下代码:3. 主 CLI 应用
这是提供命令行界面(CLI)的主 Python 文件,将所有逻辑、回调与错误处理整合在一个文件中,并高效复用其他文件中的工具与代理。
1. 先完成 CLI 所需的一切导入与设置。包含标准库、在 Windows 上确保正确的 Unicode 输出(正确显示表情与符号),并导入 OpenAI 的运行器以及我们的规划代理。
2. 接着定义命令行选项,允许您在运行工具时传入仓库与 Issue 编号。
src/app.py:
3. 然后收集输入并准备上下文。读取参数中的仓库与 Issue,确保已设置 OpenAI API Key,构建清晰的提示词,指导代理按步骤分析 Issue,并创建带时间戳的 markdown 文件用于保存最终计划。
4. 接着,我们让代理的思考过程可见。我们尝试从每个事件中提取干净的推理片段,并打印一行简短的“💭 Reasoning…”提示,让您看到模型在后台正在思考什么,而不是只展示底层细节。
5. 随后我们处理工具调用与跟踪。检测代理使用的工具,将其参数(如 repo、path 或 query)格式化为简洁字符串,打印“🔧 Calling…”消息,并维护内部的活动工具映射,以便标记完成并在后续汇总。
6. 现在我们接好流式循环与持久化。处理代理的流式事件,边到边打印令牌,实时显示推理与工具调用;若出现问题则优雅回退到非流式运行;最后将完整的执行计划写入带元数据的 markdown 文件。
7. 最后在主入口中将一切串联起来:解析参数、获取仓库与 Issue,校验环境、构建代理与提示词,使用流式方式运行规划器(必要时回退为同步运行),并保存结果。
注意:源代码、配置与文档可在 GitHub 仓库查看: kingabzpro/Issue-Analyzer。复现结果时请参考。
测试 GitHub Issue 分析器
我们的 CLI 应用有两种使用方式:交互模式,应用会逐步提示您提供仓库名与 Issue 编号;以及 CLI 模式,启动应用时需直接提供全部信息。
启动交互模式,输入以下命令:
启动后,系统会提示您提供仓库名与 Issue 编号,应用将开始通过工具与推理来协助您。
很快您就会收到该 Issue 的摘要与解决思路。摘要包含所用工具的详情,以及保存信息的 markdown 文件位置。
您可以打开该 markdown 文件查看详细的 Issue 计划。
CLI 模式需要在命令中直接包含仓库名与 Issue 参数,如下所示:
整个过程都是流式的,您将看到代理使用了哪些工具、是否在有效地进行推理,并在最后实时输出生成的响应。
后续改进
当前版本的 GitHub Issue 分析器旨在理解问题并生成准确的执行计划。
然而,代理式工作流的真正潜力在于自动化计划生成之后的所有步骤。以下是您可以在系统中实现的一些重大升级:
1. 自动分支与 PR 创建
项目目标是将计划自动转化为可运行的 Pull Request,从而简化开发流程,提升工作效率。
该功能将包括代理直接创建新分支,便于实施更改。同时,代理会基于生成的计划应用代码修改,并运行 GitHub CLI 工作流,如 gh pr create 与 gh pr view。
此外,系统将自动生成 PR 描述、变更日志与关联 Issue 引用,并智能打标签,如 Bug 修复、功能增强或重构等类型。
总体而言,此举将代理有效升级为从 Issue 到 Pull Request 的全自动化系统,显著提升开发周期中的生产力与一致性。
2. 批量 Issue 分析与批处理工具执行
该升级将通过为 GitHub Issue 引入批处理能力来提升分析流程。团队可以对多个 Issue 进行并行或排队分析,从而更有效地处理大量问题。
此外,此升级还能帮助识别重复或相关问题,提升组织管理能力。Issue 也可按复杂度、子系统或影响范围等维度进行分类。
为进一步提效,团队可批量运行 GitHub 或 Firecrawl 工具,显著提升效率。最终,这一升级可使团队通过一条命令自动分拣数十甚至上百个 Issue。
3. PR 前测试、安全检查与验证
在创建 Pull Request(PR)之前,代理应当对拟议更改进行充分验证,而不是仅仅生成代码。该验证流程应包括多项计划特性:通过 GitHub Actions 或本地测试运行器执行单元测试,确保代码行为符合预期。
依赖项验证同样关键:检查导入是否准确、是否缺失模块,并解决版本不匹配等可能影响项目的问题。同时进行 Lint、格式化与类型检查,以维护代码质量。
必须确保修改不会破坏现有构建流水线,并能检测到任何 API 的破坏性变更或回归。遵循这些步骤可确保 PR 干净、安全、可用于生产部署。
关于 GPT-5.1 Codex 的最后思考
使用 GPT-5.1 Codex 与 openai-agents 构建高级、多步骤的代理,出乎意料地简单。您只需定义自己的工具,并清晰指示模型在何时、如何使用它们即可。
本教程中我选择 GitHub CLI,因为它快速、直观、易集成;但您同样可以使用 GitHub Python SDK、直接 API 调用,或其他 CLI/Bash 工具。灵活性才是关键。
您可以按需无限扩展这一套配置。
例如,您可以创建:
本教程的目标是展示 GPT-5.1 Codex 的真正能力:轻松调用工具、理解大型代码库、执行结构化推理,并能在无需频繁人工干预的情况下运行长链路自动化。
如果您希望进一步学习如何创建 AI 代理,推荐查看我们的 Google ADK AI 代理课程,以及我们的 AI 代理项目清单。