A DevOps Agent That Reviews PRs, Diagnoses Outages, and Writes Its Own Postmortems
Chapter 11: Practical Project 2: Automated R&D and Operations Agent
In software development, a large amount of repetitive work consumes engineers' time: code reviews, bug localization, log analysis, and troubleshooting. If an Agent can take on these tasks, engineers can focus on more creative work. This chapter will build an automated R&D and operations Agent covering code review, log analysis, CI/CD integration, and knowledge accumulation.
11.1 Scenario Definition: Automated Code Review and Bug Fix Assistant
Project Goals
| Capability | Input | Output |
|---|---|---|
| Code Review | Git Diff / PR Description | Review Comments + Improvement Suggestions |
| Bug Localization | Error Logs + Code Repository | Possible Causes + Fix Plan |
| Log Analysis | Application Log Files | Anomaly Patterns + Root Cause Speculation |
| Fault Response | Alert Information | Diagnostic Steps + Repair Actions |
Architecture Design
Developer Submits PR / Alert Triggered
|
[Event Listener] ──> GitHub Webhook / Monitoring System
|
[Context Building] ──> Read Code/Logs/History
|
[Agent Analysis] ──> Review/Diagnose/Fix
|
[Result Output] ──> PR Comment / Ticket / Notification
|
[Knowledge Accumulation] ──> Write to Knowledge Base
Technology Selection
| Component | Selection | Reason |
|---|---|---|
| LLM | GPT-4o | Code understanding requires strong reasoning |
| Code Hosting | GitHub | Mature Webhook ecosystem |
| CI/CD | GitHub Actions | Seamless integration with GitHub |
| Log Storage | ELK / Loki | Structured log retrieval |
| Knowledge Base | Chroma | Vector storage for incident cases |
11.2 Context Building: Parsing Code Repositories and Dependencies
Git Repository Operations
from langchain_core.tools import tool
import subprocess
@tool
def get_git_diff(repo_path: str, base_branch: str = "main") -> str:
"""Get the code changes of the current branch relative to the base branch in a Git repository.
Args:
repo_path: Local path of the code repository
base_branch: Base branch name, default is main
"""
try:
result = subprocess.run(
["git", "diff", base_branch, "--stat"],
capture_output=True, text=True, cwd=repo_path
)
stat = result.stdout
result = subprocess.run(
["git", "diff", base_branch],
capture_output=True, text=True, cwd=repo_path
)
diff = result.stdout
# Limit diff size to avoid exceeding context window
if len(diff) > 10000:
diff = diff[:10000] + "\n... (diff truncated, total {} characters)".format(len(diff))
return f"Change Statistics:\n{stat}\n\nDetailed Diff:\n{diff}"
except Exception as e:
return f"Failed to get diff: {e}"
@tool
def read_file_content(file_path: str, repo_path: str = "", start_line: int = 1, end_line: int = -1) -> str:
"""Read the content of a code file.
Args:
file_path: Relative file path
repo_path: Repository root directory
start_line: Starting line number (starting from 1)
end_line: Ending line number (-1 means to the end of the file)
"""
full_path = os.path.join(repo_path, file_path)
try:
with open(full_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if end_line == -1:
end_line = len(lines)
selected = lines[start_line - 1:end_line]
return "".join(selected)
except Exception as e:
return f"Failed to read file: {e}"
@tool
def list_directory(dir_path: str, repo_path: str = "") -> str:
"""List directory contents.
Args:
dir_path: Relative directory path
repo_path: Repository root directory
"""
full_path = os.path.join(repo_path, dir_path)
try:
entries = os.listdir(full_path)
dirs = [e for e in entries if os.path.isdir(os.path.join(full_path, e))]
files = [e for e in entries if os.path.isfile(os.path.join(full_path, e))]
return f"Directory: {dir_path}\nSubdirectories: {dirs}\nFiles: {files}"
except Exception as e:
return f"Failed to list directory: {e}"
Dependency Analysis
@tool
def analyze_python_imports(file_path: str, repo_path: str = "") -> str:
"""Analyze the import dependencies of a Python file.
Args:
file_path: Relative path of the Python file
repo_path: Repository root directory
"""
import ast
full_path = os.path.join(repo_path, file_path)
try:
with open(full_path, 'r') as f:
tree = ast.parse(f.read())
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(alias.name)
elif isinstance(node, ast.ImportFrom):
imports.append(f"{node.module}.*" if node.module else "relative_import")
return f"Dependencies of file {file_path}:\n" + "\n".join(f" - {imp}" for imp in imports)
except Exception as e:
return f"Failed to analyze dependencies: {e}"
11.3 Interactive Debugging: How the Agent Reads Logs and Restarts Services
Log Analysis Tools
@tool
def search_logs(pattern: str, log_path: str = "/var/log/app.log",
context_lines: int = 3, max_results: int = 20) -> str:
"""Search for matching lines in a log file.
Args:
pattern: Search pattern (supports regular expressions)
log_path: Log file path
context_lines: Number of context lines
max_results: Maximum number of matches to return
"""
try:
result = subprocess.run(
["grep", "-n", "-E", f"-C{context_lines}", pattern, log_path],
capture_output=True, text=True, timeout=10
)
lines = result.stdout.split("\n")
if len(lines) > max_results:
lines = lines[:max_results] + [f"... (total {len(lines)} lines, truncated display)"]
return "\n".join(lines) if lines else "No matching logs found"
except Exception as e:
return f"Failed to search logs: {e}"
@tool
def analyze_error_patterns(log_path: str = "/var/log/app.log",
hours: int = 24) -> str:
"""Analyze error patterns in logs.
Args:
log_path: Log file path
hours: Analyze logs from the last how many hours
"""
try:
# Extract ERROR level logs
result = subprocess.run(
["grep", "-c", "ERROR", log_path],
capture_output=True, text=True
)
error_count = result.stdout.strip()
# Extract the most common error types
result = subprocess.run(
["grep", "ERROR", log_path, "|", "awk", "{print $NF}", "|",
"sort", "|", "uniq", "-c", "|", "sort", "-rn", "|", "head", "-10"],
capture_output=True, text=True, shell=True
)
top_errors = result.stdout
return f"Error statistics for the past {hours} hours:\nTotal errors: {error_count}\nTop error types:\n{top_errors}"
except Exception as e:
return f"Failed to analyze error patterns: {e}"
Service Management Tools
@tool
def restart_service(service_name: str) -> str:
"""Restart a system service (requires confirmation).
Args:
service_name: Service name
"""
# In production environments, this operation requires secondary confirmation
try:
result = subprocess.run(
["sudo", "systemctl", "restart", service_name],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
return f"Service {service_name} restarted successfully"
return f"Restart failed: {result.stderr}"
except Exception as e:
return f"Service restart exception: {e}"
@tool
def check_service_status(service_name: str) -> str:
"""Check the running status of a service.
Args:
service_name: Service name
"""
try:
result = subprocess.run(
["systemctl", "status", service_name, "--no-pager"],
capture_output=True, text=True
)
return result.stdout[:2000]
except Exception as e:
return f"Failed to check status: {e}"
Building the Operations Agent
system_prompt = """You are a senior DevOps engineer assistant.
## Responsibilities
- Analyze code changes and perform automatic code reviews
- Analyze logs and locate root causes of failures
- Restart services when necessary (requires confirmation first)
- Accumulate incident cases into the knowledge base
## Code Review Standards
- Security vulnerabilities (SQL injection, XSS, hardcoded keys)
- Performance issues (N+1 queries, memory leaks)
- Code conventions (naming, comments, error handling)
- Best practices (SOLID principles, DRY principle)
## Log Analysis Method
1. First, count error frequencies to find high-frequency errors
2. Analyze error context to find trigger conditions
3. Trace the call chain to locate the root cause
4. Search historical cases to see if it's a known issue
## Security Rules
- Must confirm with the user before restarting services
- Do not execute any irreversible operations (such as deleting data)
- Sensitive information (keys, passwords) must be masked
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
tools = [
get_git_diff, read_file_content, list_directory,
analyze_python_imports, search_logs, analyze_error_patterns,
check_service_status, restart_service,
]
llm = ChatOpenAI(model="gpt-4o", temperature=0)
agent = create_openai_tools_agent(llm, tools, prompt)
devops_agent = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=10)
11.4 CI/CD Pipeline Integration: Combining GitHub Actions with the Agent
GitHub Webhook Listener
from fastapi import FastAPI, Request
import hmac, hashlib
app = FastAPI()
WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET")
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify GitHub Webhook signature"""
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhook/github")
async def handle_github_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
if not verify_signature(payload, signature):
return {"status": "unauthorized"}
data = json.loads(payload)
event = request.headers.get("X-GitHub-Event")
if event == "pull_request" and data["action"] in ["opened", "synchronize"]:
# Trigger code review when a PR is created or updated
pr_info = {
"repo": data["repository"]["full_name"],
"pr_number": data["number"],
"diff_url": data["pull_request"]["diff_url"],
}
# Asynchronously trigger Agent review
review_result = devops_agent.invoke({
"input": f"Review code changes for PR #{pr_info['pr_number']}: {pr_info['diff_url']}"
})
# Post review results as a comment on the PR
post_pr_comment(pr_info, review_result["output"])
return {"status": "ok"}
GitHub Actions Integration
# .github/workflows/ai-code-review.yml
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
ai-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Diff
id: diff
run: |
DIFF=$(git diff origin/main...HEAD)
echo "diff<<EOF" >> $GITHUB_OUTPUT
echo "$DIFF" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: AI Review
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python3 scripts/ai_review.py --diff "${{ steps.diff.outputs.diff }}" --pr ${{ github.event.pull_request.number }}
- name: Post Review Comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 scripts/post_comment.py --pr ${{ github.event.pull_request.number }}
Auto-commenting Review Results
import requests
def post_pr_comment(pr_info: dict, review_content: str):
"""Post review results as a comment on the PR"""
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
repo = pr_info["repo"]
pr_number = pr_info["pr_number"]
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
}
comment_body = f"## AI Code Review\n\n{review_content}"
requests.post(url, json={"body": comment_body}, headers=headers)
Reference Documentation: GitHub Webhooks Documentation
11.5 Operations Knowledge Accumulation: Incident Postmortems and Automatic Knowledge Base Updates
Structuring Incident Cases
After each incident is handled, the Agent automatically structures the case and writes it to the knowledge base:
from pydantic import BaseModel
from datetime import datetime
class IncidentCase(BaseModel):
"""Incident Case"""
incident_id: str
title: str
symptoms: list[str] # Incident symptoms
root_cause: str # Root cause
resolution: str # Solution
timeline: list[dict] # Timeline
affected_services: list[str] # Affected services
severity: str # Severity level
tags: list[str] # Tags (for retrieval)
created_at: str
class IncidentKnowledgeBase:
"""Incident Knowledge Base"""
def __init__(self, vectorstore):
self.vectorstore = vectorstore
self.llm = ChatOpenAI(model="gpt-4o", temperature=0)
def add_incident(self, case: IncidentCase):
"""Add an incident case to the knowledge base"""
# Convert the case into retrievable text
doc_text = f"""
Incident Title: {case.title}
Symptoms: {"; ".join(case.symptoms)}
Root Cause: {case.root_cause}
Solution: {case.resolution}
Severity Level: {case.severity}
Tags: {", ".join(case.tags)}
"""
self.vectorstore.add_texts(
texts=[doc_text],
metadatas=[{
"incident_id": case.incident_id,
"severity": case.severity,
"services": ",".join(case.affected_services),
"created_at": case.created_at,
}]
)
def search_similar_incidents(self, symptoms: str, k: int = 3) -> list[str]:
"""Search for similar incident cases"""
retriever = self.vectorstore.as_retriever(search_kwargs={"k": k})
docs = retriever.invoke(symptoms)
return [doc.page_content for doc in docs]
def auto_create_incident(self, diagnosis: str, resolution: str) -> IncidentCase:
"""Automatically create an incident case from diagnosis and solution"""
extract_prompt = f"""
Please extract a structured incident case from the following diagnostic information:
Diagnosis Process: {diagnosis}
Solution: {resolution}
Please output in JSON format, including fields:
title, symptoms(array), root_cause, resolution, affected_services(array), severity(high/medium/low), tags(array)
"""
response = self.llm.invoke(extract_prompt)
data = json.loads(response.content)
return IncidentCase(
incident_id=f"INC-{datetime.now().strftime('%Y%m%d%H%M%S')}",
title=data["title"],
symptoms=data["symptoms"],
root_cause=data["root_cause"],
resolution=data["resolution"],
timeline=[],
affected_services=data["affected_services"],
severity=data["severity"],
tags=data["tags"],
created_at=datetime.now().isoformat(),
)
Automated Incident Postmortems
class PostMortemGenerator:
"""Automatically generate incident postmortem reports"""
def __init__(self, llm):
self.llm = llm
def generate(self, incident: IncidentCase) -> str:
template = f"""# Incident Postmortem Report: {incident.title}
## Basic Information
- Severity Level: {incident.severity}
- Affected Services: {", ".join(incident.affected_services)}
- Occurrence Time: {incident.created_at}
## Incident Symptoms
{chr(10).join(f"- {s}" for s in incident.symptoms)}
## Root Cause Analysis
{incident.root_cause}
## Solution
{incident.resolution}
## Improvement Measures
Based on the above information, please propose 3-5 improvement measures to prevent similar issues from happening again.
"""
# Use LLM to supplement improvement measures
full_report = self.llm.invoke(template).content
return full_report
Complete Operations Agent Workflow
class DevOpsWorkflow:
"""Complete DevOps Agent Workflow"""
def __init__(self, agent_executor, knowledge_base):
self.agent = agent_executor
self.kb = knowledge_base
def handle_incident(self, alert_info: str) -> dict:
"""Complete process for handling an alert event"""
# 1. First, search historical cases
similar_cases = self.kb.search_similar_incidents(alert_info)
context = f"Similar Historical Cases:\n{chr(10).join(similar_cases)}" if similar_cases else "No similar historical cases"
# 2. Agent diagnosis
diagnosis = self.agent.invoke({
"input": f"Alert Information: {alert_info}\n\n{context}\n\nPlease diagnose and provide a solution."
})
# 3. Automatically create an incident case
incident = self.kb.auto_create_incident(
diagnosis["output"],
"To be confirmed" # Solution needs manual confirmation before updating
)
# 4. Store in knowledge base
self.kb.add_incident(incident)
return {
"incident_id": incident.incident_id,
"diagnosis": diagnosis["output"],
"similar_past_incidents": len(similar_cases),
}
Chapter Summary
| Module | Core Implementation | Key Points |
|---|---|---|
| Scenario Definition | Code Review + Log Analysis + Fault Response | Clearly define Agent responsibility boundaries |
| Context Building | Git Operations + File Reading + Dependency Analysis | Agent needs sufficient code context |
| Interactive Debugging | Log Search + Error Pattern Analysis + Service Management | Dangerous operations require secondary confirmation |
| CI/CD Integration | Webhook + GitHub Actions | Automatic triggering is more reliable than manual triggering |
| Knowledge Accumulation | Structured Cases + Vector Retrieval | Every incident is knowledge accumulation |
In the next chapter, we will build the third practical project—a personalized education tutoring Agent.