一个简单的GitHub AI Agent 实现指南

GitHub AI Agent 实现指南

本文档详细记录了我们在 py-github-agent 项目中如何从零开始构建一个能够与 GitHub 交互的 AI Agent。


1. 系统架构

我们的 Agent 遵循模块化设计原则,主要由以下几层组成:
Agent 内部循环 POST /chat/ask 调用 ainvoke Prompt + History 决定行动 执行工具 调用 GitHub API 返回结果 最终答案 响应 JSON Gemini LLM (大脑) Agent Executor GitHub 工具 (手臂) GitHub 用户 API 路由 LLM 服务


2. 核心代码实现与解析

2.0. 基础服务层 (The Foundation)

在构建 Agent 工具之前,我们首先实现了一个底层的 GitHubService,用于直接与 GitHub API 交互。

文件 : src/services/github_service.py

python 复制代码
class GitHubService:
    BASE_URL = "https://api.github.com"

    def __init__(self, token: str = None):
        # 从环境变量读取 GITHUB_TOKEN
        self.token = token or os.getenv("GITHUB_TOKEN")
        # ... 设置 headers ...

    async def _make_request(self, url: str, params: Dict = None) -> Any:
        """封装 aiohttp 请求,处理基本的错误和状态检查"""
        try:
            async with aiohttp.ClientSession(headers=self.headers) as session:
                async with session.get(url, params=params) as response:
                    response.raise_for_status()
                    return await response.json()
        except Exception as e:
            logger.error(f"Error making request to {url}: {e}")
            return None

    async def get_all_files_list(self, repo_owner: str, repo_name: str, branch: str = "main") -> List[str]:
        """
        高效获取仓库所有文件列表。
        使用 GitHub Git Trees API 的 recursive=1 参数。
        """
        # 直接调用 Git Trees API,避免多次请求
        url = f"{self.BASE_URL}/repos/{repo_owner}/{repo_name}/git/trees/{branch}?recursive=1"
        
        tree_data = await self._make_request(url)
        
        # 过滤出文件(blob),忽略目录(tree)
        file_paths = [item["path"] for item in tree_data["tree"] if item.get("type") == "blob"]
        return file_paths

解析:

  • 异步通信 : 使用 aiohttp 库实现全异步的 HTTP 请求,确保在高并发下的性能。
  • 高效 API : 利用 GitHub 的 Git Trees API (recursive=1),仅需一次 HTTP 请求即可获取整个仓库的文件结构,避免了递归遍历目录所需的多次请求。

2.1. 工具层 (The Arms)

有了基础服务后,我们需要将其封装为 Agent 可以理解的 Tool。

文件 : src/tools/github_tools.py

python 复制代码
# 定义输入参数的 Schema,这对 LLM 理解工具至关重要
class ListRepoFilesInput(BaseModel):
    """Input for the list_repository_files tool."""
    repo_owner: str = Field(description="The owner of the GitHub repository.")
    repo_name: str = Field(description="The name of the GitHub repository.")
    branch: str = Field(description="The branch to list files from.", default="main")

class ListRepoFilesTool(BaseTool):
    name: str = "list_repository_files"
    description: str = "Useful for listing all file paths in a specific branch of a GitHub repository."
    args_schema: Type[BaseModel] = ListRepoFilesInput

    # 实际执行逻辑:调用底层的 GitHubService
    async def _arun(self, repo_owner: str, repo_name: str, branch: str = "main") -> List[str]:
        logger.info("Running ListRepoFilesTool asynchronously...")
        return await github_service.get_all_files_list(repo_owner, repo_name, branch)

解析:

  • 我们继承了 BaseTool。
  • args_schema 和 description 是最关键的。LLM 会读取这些信息来决定是否调用此工具,以及如何提取参数。

2.2. 模型层 (The Brain)

我们需要一个能够进行逻辑推理的 LLM。我们封装了 Google Gemini。

文件 : src/llm/custom_gemini.py (部分展示)

python 复制代码
class CustomGeminiChatModel(BaseChatModel):
    def __init__(self, **kwargs: Any):
        # ... 初始化代码 ...
        # 关键配置:强制使用 REST 传输以适应代理环境
        self.client = ChatGoogleGenerativeAI(
            model="gemini-2.5-pro",
            transport="rest",  # <-- 解决网络卡顿的关键
            **kwargs
        )
    
    # 必须实现 _generate 方法供 LangChain 调用
    def _generate(self, messages, ...):
        llm_result = self.client.generate([messages], ...)
        return ChatResult(generations=llm_result.generations[0])

解析:

  • 我们创建了一个自定义类来统一管理 API Key 和网络配置(如 transport="rest")。
  • 它继承自 BaseChatModel,这使得它可以无缝集成到 LangChain 的任何组件中。

2.3. Agent 组装 (The Assembly)

我们将 LLM 和工具组装成一个可以运行的 Agent。

文件 : src/agents/github_agent.py

python 复制代码
def create_github_agent() -> Runnable:
    # 1. 准备工具箱
    tools = [list_repo_files_tool]
    
    # 2. 准备大脑
    llm = CustomGeminiChatModel(temperature=0) # 0温度保证输出精确

    # 3. 准备系统提示词 (System Prompt)
    # 这是一个 ReAct 风格的 Prompt,指导 LLM 如何思考
    prompt = ChatPromptTemplate.from_messages([...])

    # 4. 创建 Agent (大脑 + 工具 + 提示词)
    agent = create_tool_calling_agent(llm, tools, prompt)

    # 5. 创建执行器 (身体)
    # AgentExecutor 负责运行 Agent 的思考-行动循环
    agent_executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True, # 开启日志以便调试
        handle_parsing_errors=True
    )

    return agent_executor

解析:

  • 这个工厂函数返回一个 AgentExecutor。
  • AgentExecutor 是一个实现了 Runnable 接口的对象,这意味着它可以像普通函数一样被调用。

2.4. 服务层与 API (The Interface)

最后,我们将 Agent 暴露给外部世界。

文件 : src/services/llm_service.py

python 复制代码
class LLMService:
    # 这里的 runnable 可以是 LLM,也可以是 AgentExecutor
    def __init__(self, runnable: Runnable):
        self.runnable = runnable

    async def ainvoke(self, prompt: str):
        # 统一调用入口
        return await self.runnable.ainvoke(prompt)

文件 : src/routers/chat_router.py

python 复制代码
@router.post("/ask", response_model=AskResponse)
async def ask(
    request: AskRequest,
    llm_service: LLMService = Depends(get_llm_service)
):
    # 处理 HTTP 请求,调用 Service
    response = await llm_service.ainvoke(request.query)
    return AskResponse(answer=response['output'])

测试输出

bash 复制代码
2025-11-29 21:17:55.280 | INFO     | test_github_agent:test_github_agent_via_llm_service:29 - Invoking agent with prompt: 'Can you list all the files in the 'nvd11/py-github-agent' repository?'
2025-11-29 21:17:55.280 | INFO     | src.services.llm_service:ainvoke:12 - LLMService ainvoking with prompt: {'input': "Can you list all the files in the 'nvd11/py-github-agent' repository?"}


> Entering new AgentExecutor chain...
Action:
```json
{
  "action": "list_repository_files",
  "action_input": {
    "repo_owner": "nvd11",
    "repo_name": "py-github-agent"
  }
}
```2025-11-29 21:17:59.438 | INFO     | src.tools.github_tools:_arun:32 - Running ListRepoFilesTool asynchronously...
2025-11-29 21:17:59.438 | INFO     | src.services.github_service:get_all_files_list:77 - Fetching file list for nvd11/py-github-agent on branch main
2025-11-29 21:17:59.987 | SUCCESS  | src.services.github_service:get_all_files_list:93 - Successfully fetched 44 file paths.

Observation: ['.dockerignore', '.gitignore', '.vscode/launch.json', '.vscode/settings.json', 'Dockerfile', 'README.md', 'cloudbuild-helm.yaml', 'cloudbuild_cloudrun.yaml', 'docs/LANGCHAIN_MODEL_GUIDE.md', 'docs/TROUBLESHOOTING.md', 'docs/agent.md', 'docs/get-files-agent.md', 'gateway_routing_explained.md', 'get_helm.sh', 'helm/Chart.yaml', 'helm/templates/deployment.yaml', 'helm/templates/service.yaml', 'helm/values.yaml', 'k8s/gateway.yaml', 'k8s/httproute.yaml', 'pytest.ini', 'requirements.txt', 'server.py', 'src/configs/__init__.py', 'src/configs/config.py', 'src/configs/config_dev.yaml', 'src/configs/config_local.yaml', 'src/configs/config_prod.yaml', 'src/configs/log_config.py', 'src/configs/proxy.py', 'src/llm/custom_deepseek.py', 'src/llm/custom_gemini.py', 'src/llm/factory.py', 'src/llm/gemini_client.py', 'src/main.py', 'src/routers/chat_router.py', 'src/schemas/chat_schemas.py', 'src/services/github_service.py', 'src/services/llm_service.py', 'test.py', 'test/services/test_github_service.py', 'test/services/test_llm_service.py', 'test/test_1.py', 'test/test_gemini.py']
Thought:This is the full list of files in the `nvd11/py-github-agent` repository:

*   .dockerignore
*   .gitignore
*   .vscode/launch.json
*   .vscode/settings.json
*   Dockerfile
*   README.md
*   cloudbuild-helm.yaml
*   cloudbuild_cloudrun.yaml
*   docs/LANGCHAIN_MODEL_GUIDE.md
*   docs/TROUBLESHOOTING.md
*   docs/agent.md
*   docs/get-files-agent.md
*   gateway_routing_explained.md
*   get_helm.sh
*   helm/Chart.yaml
*   helm/templates/deployment.yaml
*   helm/templates/service.yaml
*   helm/values.yaml
*   k8s/gateway.yaml
*   k8s/httproute.yaml
*   pytest.ini
*   requirements.txt
*   server.py
*   src/configs/__init__.py
*   src/configs/config.py
*   src/configs/config_dev.yaml
*   src/configs/config_local.yaml
*   src/configs/config_prod.yaml
*   src/configs/log_config.py
*   src/configs/proxy.py
*   src/llm/custom_deepseek.py
*   src/llm/custom_gemini.py
*   src/llm/factory.py
*   src/llm/gemini_client.py
*   src/main.py
*   src/routers/chat_router.py
*   src/schemas/chat_schemas.py
*   src/services/github_service.py
*   src/services/llm_service.py
*   test.py
*   test/services/test_github_service.py
*   test/services/test_llm_service.py
*   test/test_1.py
*   test/test_gemini.py

> Finished chain.
2025-11-29 21:18:04.758 | INFO     | src.services.llm_service:ainvoke:14 - LLMService ainvocation complete.
2025-11-29 21:18:04.759 | SUCCESS  | test_github_agent:test_github_agent_via_llm_service:73 - Agent finished with final answer:
This is the full list of files in the `nvd11/py-github-agent` repository:

*   .dockerignore
*   .gitignore
*   .vscode/launch.json
*   .vscode/settings.json
*   Dockerfile
*   README.md
*   cloudbuild-helm.yaml
*   cloudbuild_cloudrun.yaml
*   docs/LANGCHAIN_MODEL_GUIDE.md
*   docs/TROUBLESHOOTING.md
*   docs/agent.md
*   docs/get-files-agent.md
*   gateway_routing_explained.md
*   get_helm.sh
*   helm/Chart.yaml
*   helm/templates/deployment.yaml
*   helm/templates/service.yaml
*   helm/values.yaml
*   k8s/gateway.yaml
*   k8s/httproute.yaml
*   pytest.ini
*   requirements.txt
*   server.py
*   src/configs/__init__.py
*   src/configs/config.py
*   src/configs/config_dev.yaml
*   src/configs/config_local.yaml
*   src/configs/config_prod.yaml
*   src/configs/log_config.py
*   src/configs/proxy.py
*   src/llm/custom_deepseek.py
*   src/llm/custom_gemini.py
*   src/llm/factory.py
*   src/llm/gemini_client.py
*   src/main.py
*   src/routers/chat_router.py
*   src/schemas/chat_schemas.py
*   src/services/github_service.py
*   src/services/llm_service.py
*   test.py
*   test/services/test_github_service.py
*   test/services/test_llm_service.py
*   test/test_1.py
*   test/test_gemini.py
.

=============================== warnings summary ===============================
test/agents/test_github_agent.py::test_github_agent_via_llm_service
  /home/gateman/projects/github/py-github-agent/src/agents/github_agent.py:20: LangChainDeprecationWarning: LangChain agents will continue to be supported, but it is recommended for new use cases to be built with LangGraph. LangGraph offers a more flexible and full-featured framework for building agents, including support for tool-calling, persistence of state, and human-in-the-loop workflows. For details, refer to the [LangGraph documentation](https://langchain-ai.github.io/langgraph/) as well as guides for [Migrating from AgentExecutor](https://python.langchain.com/docs/how_to/migrate_agent/) and LangGraph's [Pre-built ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/).
    agent_executor = initialize_agent(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 1 passed, 1 warning in 10.70s =========================
Finished running tests!

3. 总结

通过这一系列步骤,我们实现了一个灵活且强大的架构:

  1. 模块化:工具、模型、代理逻辑完全分离。
  2. 可扩展性 :如果想添加新功能(如读取文件内容),只需在 github_tools.py 中添加新工具,并在 github_agent.py 的列表中注册即可。
  3. 统一接口 :通过 Runnable 接口和 LLMService,上层应用不需要知道底层是简单的 LLM 还是复杂的 Agent。
相关推荐
waterfeeling3 小时前
用 Context7 做一次点云配准的调研—测试—报告闭环:鲁棒 ICP
人工智能
Data-Miner7 小时前
面对 AI 大模型智能涌现,如何从技术层面保护好隐私与数据安全?
大数据·人工智能
hpoenixf7 小时前
当 AI 开始谈“买什么”,研究助手离投顾和交易还有多远?
人工智能
大模型真好玩7 小时前
如何让视频模型对你言听计从?结构化提示词助你生成电影级画面
人工智能·agent·deepseek
zhang133830890758 小时前
传统机械三杯式风速传感器易磨损冻堵?CG-88金属超声波风速风向传感器给出免运维方案
运维·服务器·网络·人工智能·自动化
特立独行的猫a8 小时前
会说确认叫车才下单-启元机器人官方滴滴打车技能源码详解
人工智能·机器人·技能·启元机器人
老金带你玩AI8 小时前
GPT-6.1 Terra上线:更便宜了,干活到底怎么样?
人工智能
会议咨询8 小时前
2027年电力电子、人工智能与大数据国际会议(PEABD 2027)
大数据·人工智能·电力电子
Jsundoku8 小时前
第五章 深度学习 -- 卷积神经网络
人工智能·深度学习·cnn
水如烟9 小时前
孤能子视角:SEP《人工智能的认识论》EIS读法·理解——理解幻觉与解压缩幻觉:同一缺口,两份报告
人工智能