最新资讯
stackoverflow.blog 欢迎来到芯片时代:半导体制造、AI 数据中心与芯片设计的未来 www.youtube.com AI Agent 总犯同样的错:CentralAgent AI 将记忆重构为经验系统 pub.towardsai.net Netflix 科学家建议重采样 LLM 评判:5 次仅将误差棒缩小 13% pub.towardsai.net Jev 作为 LLM 前的静默层:构建提示感知的模型路由器 reddit.com OpenAI 称 Astra 仅满足 10% 安全任务标准 dev.to FreeToken:在自有 GPU 上运行前沿 MoE 模型及其内存需求 dev.to 当 AI Agent 说“完成”时,究竟完成了什么? dev.to 部署 Underdog Bark 3.0 Pro:逐字转写、自定义后端与分块设置 dev.to 为艰难对话构建 AI:五个 Agent、一条安全正则与一位盲评监督者 techcrunch.com Palo Alto Networks 洽谈以 5 亿美元收购 AI Agent 安全初创公司 Clutch theinformation.com 黄仁勋被要求就涉华间谍指控向国会作简报 www.infoq.com Edera 推出 Object Capability Bounded Authority,试图将凭证与 AI Agent 工作负载隔离
多元动态
登录 注册
智能体 dev.to · @Naor Peled

用 PR-Agent 和 CDK 在 AWS Lambda 上运行无服务器 AI 代码审查代理

原文 我的收藏

文章介绍如何自托管代码审查代理 PR-Agent。PR-Agent 是开源项目,支持各类 AI 模型提供商和本地模型,工作流程为:开发者提交 PR 后,代理更新描述并发布代码建议,也可通过 /review、/improve、/ask 等斜杠命令在 PR 中交互。项目最初由 Qodo 发起,去年捐赠给社区,现由独立 GitHub 组织维护。文章演示如何用 AWS Lambda 部署,并称文中步骤可克隆直接运行。

AI推荐理由 介绍如何用 AWS Lambda 和 CDK 自托管开源代码审查代理 PR-Agent,包含具体部署路径、支持的模型提供商和 PR 内斜杠命令交互方式,并说明项目由 Qodo 发起后捐赠给社区独立维护。属于可操作的工程实践内容,对希望自建 AI 代码审查的团队有直接参考价值。

68

正文

Code reviews are an essential part of our day to day work. Their importance and way of execution have changed a bit in the GenAI era. In this post, I'll show you how you can self host your own code review agent. What is PR-Agent PR-Agent is an open-source project that allows you to perform code reviews using a customizable AI agent in a self-hosted way. It works with all common AI model providers and local models. The flow is the following: You open a pull request, and PR-Agent updates the description and posts a set of code suggestions. You can also talk to it in the PR with slash commands like /review, /improve or /ask what does this migration do?. The project initially started at Qodo, which works on an enterprise code review solution, and was donated to the community last year. It now lives in its own GitHub org and is maintained by the community. In this post, we'll explore how to deploy it using AWS Lambda. If you prefer to just clone and run it, everything in this post is also in pr-agent-lambda-cdk. A few words about me Hi, I'm Naor, a software engineer for the past 7 years professionally and an open source contributor/maintainer for about 6 years. I am also an AWS Community Builder in the DevTools category and love sharing insights about modern developer workflows and cloud tooling. I'm the lead maintainer of PR-Agent and I also maintain projects like TypeORM and a few others you can find on my GitHub page. I currently work for groundcover, where we're building a BYOC, eBPF powered observability solution. A small overview on how PR-Agent can be used PR-Agent is a Python project that has two modes: CLI. A FastAPI server that listens to requests from different git providers. You can override the default configurations by creating a configuration file. The configuration file needs to be named .pr_agent.toml and can sit in two locations: In your repo's root dir. In a repo called pr-agent-settings that sits under your git provider's org. If you work on a monorepo, you can also add a .pr_agent.toml inside sub-directories, for example one per service. PR-Agent applies the ones on the path of the files changed in the PR, and the closest one wins. These files can change things like the model, the ignored files and the settings of each tool, while some settings can only be set in the root file. It's off by default, to turn it on set enable_per_directory_settings = true under [config] in your root .pr_agent.toml. For now it works on GitHub and GitLab. Every setting can also be set with an environment variable, using the section and the key with a double underscore between them. For example, webhook_secret under [github] becomes GITHUB__WEBHOOK_SECRET. Environment variables are applied last, so they win over .pr_agent.toml. We'll use them a lot, since on Lambda there's no config file to mount. Model calls go through LiteLLM, so moving from OpenAI to Bedrock or to a local model is just a config change. For big PRs, PR-Agent uses a compression strategy that fits the most relevant parts of the diff into a fixed token budget, so even big PRs fit. Why Lambda The GitHub Action is the quickest way to get started, and for a single repo it works great. Once you have several repos, a git provider other than GitHub, or you don't want model credentials in your CI, it's easier to have one deployment that everything talks to. For that I like Lambda, for a few reasons: A review only happens when something happens on a PR, and there's nothing to do in between. Running a container 24/7 for something that fires a few dozen times a day means paying for idle time. Every setting can be passed as an environment variable, so there's no config file or volume to mount. The token budget also limits how long a review can take, so it finishes well within Lambda's 15 minute limit. Nothing has to be kept between invocations. PR-Agent fetches everything it needs from the git provider's API. PR-Agent already supports Lambda. This is the GitHub Lambda handler, pr_agent/servers/github_lambda_webhook.py (I removed the logging from the except): from fastapi import FastAPI from mangum import Mangum from starlette.middleware import Middleware from starlette_context.middleware import RawContextMiddleware from pr_agent.servers.github_app import router try: from pr_agent.config_loader import apply_secrets_manager_config apply_secrets_manager_config() except Exception as e: ... # falls back to environment variables middleware = [Middleware(RawContextMiddleware)] app = FastAPI(middleware=middleware) app.include_router(router) handler = Mangum(app, lifespan="off") def lambda_handler(event, context): return handler(event, context) Enter fullscreen mode Exit fullscreen mode It uses the same FastAPI router as the regular server, wraps it with Mangum, which translates Lambda events into ASGI requests, and loads the config from Secrets Manager on cold start. So the regular server and the Lambda deployment share the same code. The review runs inside the invocation On Lambda, PR-Agent runs the whole review before it responds to the webhook. That's actually what we want, because Lambda freezes the function once it returns, so a review running in the background would get stuck. It does mean the function needs a long timeout (the docs recommend at least 3 minutes). The side effect is that GitHub shows every delivery as timed out, since it only waits 10 seconds. The comments are still posted, so you can ignore it. GitLab.com is stricter, and disables the webhook after a few timeouts in a row. The fix is a small function in front of PR-Agent that calls it asynchronously and responds right away. I left it out of this post to keep it short, but the companion repo has it (ASYNC_REVIEWS=true), and it's on by default for every provider except GitHub. The AWS pieces PR-Agent ships with heavy Python dependencies, so a zip deployment is not going to be fun. Lambda container images can be up to 10 GB uncompressed, compared to 250 MB unzipped for zips. Lambda pulls the image from ECR, in the same region as the function. In front of the function we'll use a Lambda Function URL, a dedicated HTTPS endpoint without API Gateway. A webhook receiver doesn't need routing, stages or usage plans, but the main reason is the timeout. API Gateway times out after 29 seconds by default, and as we saw, the review runs inside the request. A Function URL waits for the function's own timeout. It uses authType: NONE, since GitHub can't sign requests with SigV4, and PR-Agent checks the webhook's HMAC signature itself. The tradeoff is no WAF, no usage plans, and no custom domain unless you put CloudFront in front of it. GitHub App private keys and webhook secrets go in Secrets Manager and not in environment variables, where anyone with console read access can see them. PR-Agent already knows how to read them from there. For the model I'm using Amazon Bedrock, which keeps everything inside your own AWS account. LiteLLM supports Bedrock, so PR-Agent does too. And all of it is defined in CDK, which compiles down to CloudFormation but lets you write actual TypeScript instead of YAML. Deploying it Before you start You'll need: An AWS account with the CLI v2 configured, and credentials that can create IAM roles. Docker running locally. Node 20 or newer, jq and openssl. Pick a region and use it for everything. I'll use us-east-1. Then pick a model and make sure you have access to it before you build anything. Otherwise you'll end up debugging an IAM policy that's actually fine. I'll use Claude Sonnet 4.5, with Haiku 4.5 as the fallback. Anthropic models need a First Time Use form, which you submit from the model catalog in the Bedrock console, either in each account or once from your org's management account. Without it, calls fail with an error saying the use case details weren't submitted. The first call to a model that's sold through AWS Marketplace also creates a Marketplace subscription, and the caller needs aws-marketplace:Subscribe for that. The Lambda's role doesn't have it, so invoke each model once from the Bedrock console playground as an admin. If you skip this you'll get AccessDeniedException, sometimes only after the first few calls worked. The model access docs list what each model needs. Any model LiteLLM can call on Bedrock works. After the stack, I'll show how to switch to DeepSeek V3.2, which doesn't need either of these steps. 1. Create the GitHub App PR-Agent authenticates as a GitHub App, so let's start there. Go to Settings, Developer settings, GitHub Apps and create a new one: Permissions: Pull requests and Issues read and write, Contents and Metadata read only. Events: Pull request, Pull request review, Pull request review comment and Issue comment. Under Webhook, tick Active, put https://example.com as the URL for now, and set a webhook secret. GitHub won't save an active webhook without a URL, and we'll only get the real one in step 6. Generate the secret in the same shell you'll use for the rest of the steps, since the next commands read it from there: export WEBHOOK_SECRET=$(openssl rand -hex 32) echo $WEBHOOK_SECRET # paste this into the GitHub App form Enter fullscreen mode Exit fullscreen mode Generate a private key, and copy the App ID from the top of the settings page: export APP_ID=123456 export PEM=~/Downloads/your-app.2026-09-27.private-key.pem Enter fullscreen mode Exit fullscreen mode Then install the App on the repos you want reviewed. The install docs have more details. 2. Put the credentials in Secrets Manager PR-Agent reads a single JSON secret, where the config keys are flattened into dotted strings. The private key has to be a single JSON string with escaped newlines. jq can build the whole file for us: jq -Rs --arg app_id "$APP_ID" --arg secret "${WEBHOOK_SECRET:?run step 1 first}" \ '{"github.app_id": $app_id, "github.webhook_secret": $secret, "github.private_key": .}' \ config.json Enter fullscreen mode Exit fullscreen mode That gives you: { "github.app_id": "123456", "github.webhook_secret": "your-webhook-secret", "github.private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----\n" } Enter fullscreen mode Exit fullscreen mode Store it, and then delete the local copies. You don't need them anymore, and GitHub can generate a new key whenever you need one: aws secretsmanager create-secret \ --name pr-agent/config \ --secret-string file://config.json \ --region us-east-1 \ && rm config.json "$PEM" Enter fullscreen mode Exit fullscreen mode Make sure to pass --region. On Lambda, the secret is looked up in the function's region, so a secret in another region won't be found. When that happens, PR-Agent logs Failed to get secrets from AWS Secrets Manager on cold start and continues without credentials, and every webhook after that gets a 403 with Webhook secret not configured. Two more things to know about this secret: Only keys with exactly one dot are read. Anything deeper is silently ignored. A key is only applied if the setting is unset or empty. So the secret is only for credentials. To change a setting that has a default, use a SECTION__KEY environment variable. 3. Copy the image into ECR The project publishes Lambda images to Docker Hub on every release: pragent/pr-agent:-github_lambda, a rolling pragent/pr-agent:github_lambda tag, and the same for GitLab. So there's no need to build one yourself. I'm using 0.46.0, which is the latest at the time of writing. You do need to copy it into your own ECR, because that's the only registry Lambda pulls from. The published tags are multi-arch (amd64 and arm64), and Lambda doesn't accept multi-arch images, so pull a single platform. I'd also pin a version and not use the rolling tag, so a redeploy a few months from now doesn't quietly pick up a different build. ACCOUNT=$(aws sts get-caller-identity --query Account --output text) REGION=us-east-1 TAG=0.46.0-github_lambda aws ecr create-repository --repository-name pr-agent --region $REGION aws ecr get-login-password --region $REGION \ | docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com docker pull --platform linux/amd64 pragent/pr-agent:$TAG docker tag pragent/pr-agent:$TAG $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/pr-agent:$TAG docker push $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/pr-agent:$TAG Enter fullscreen mode Exit fullscreen mode If you use Docker Desktop with the containerd image store, docker push might push the multi-arch index anyway. Run docker manifest inspect on the ECR tag. If you see a layers array you're good, if you see manifests, Lambda will reject it. 4. The CDK stack The stack looks up the secret from step 2 and the ECR repo from step 3 by name, and the image has to exist when you deploy. That's why we did those first. mkdir pr-agent-infra && cd pr-agent-infra npx cdk init app --language typescript Enter fullscreen mode Exit fullscreen mode cdk init installs the dependencies and names things after the directory, so the stack goes in lib/pr-agent-infra-stack.ts. Replace what's there with: import * as cdk from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as logs from 'aws-cdk-lib/aws-logs'; import { Construct } from 'constructs'; // the tag you pushed in step 3 const IMAGE_TAG = '0.46.0-github_lambda'; // cross-region inference profile ids, the "us." prefix is what makes them profiles const MODEL = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0'; const FALLBACK_MODEL = 'us.anthropic.claude-haiku-4-5-20251001-v1:0'; export class PrAgentInfraStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const repo = ecr.Repository.fromRepositoryName(this, 'PrAgentRepo', 'pr-agent'); const config = secretsmanager.Secret.fromSecretNameV2( this, 'PrAgentConfig', 'pr-agent/config', ); const fn = new lambda.DockerImageFunction(this, 'PrAgentFunction', { code: lambda.DockerImageCode.fromEcr(repo, { tagOrDigest: IMAGE_TAG }), // has to match the platform you pulled architecture: lambda.Architecture.X86_64, // memory buys CPU on Lambda, and 2 GB is a reasonable spot for this memorySize: 2048, // the review runs inside the invocation, and a big PR runs describe, // review and improve back to back. you only pay for the time used timeout: cdk.Duration.minutes(15), // caps the blast radius when someone merges a train of 30 PRs. // new accounts start on a reduced concurrency quota, often 10. there, // drop this line: Lambda keeps 100 unreserved, so reserving fails reservedConcurrentExecutions: 5, // Lambda log groups never expire by default logGroup: new logs.LogGroup(this, 'PrAgentLogs', { retention: logs.RetentionDays.ONE_MONTH, removalPolicy: cdk.RemovalPolicy.DESTROY, }), environment: { // env vars can't contain dots, and Dynaconf reads SECTION__KEY into section.key CONFIG__GIT_PROVIDER: 'github', CONFIG__PUBLISH_OUTPUT: 'true', CONFIG__MODEL: `bedrock/${MODEL}`, CONFIG__FALLBACK_MODELS: `["bedrock/${FALLBACK_MODEL}"]`, // the default of 32000 is a fraction of what current models take CONFIG__MAX_MODEL_TOKENS: '128000', CONFIG__SECRET_PROVIDER: 'aws_secrets_manager', AWS_SECRETS_MANAGER__SECRET_ARN: config.secretArn, // read straight from the process env by the model layer. on Lambda it // makes boto3 resolve the execution role credentials the runtime // injects, so there are no static keys anywhere AWS_USE_IMDS: 'true', AWS_REGION_NAME: this.region, // /tmp is the only writable path in a Lambda container AZURE_DEVOPS_CACHE_DIR: '/tmp', HOME: '/tmp', }, }); config.grantRead(fn); // 1. the cross-region inference profile, in your account fn.addToRolePolicy( new iam.PolicyStatement({ actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], resources: [ `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${MODEL}`, `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${FALLBACK_MODEL}`, ], }), ); // 2. the underlying foundation models, in every region the profile can route to. // foundation-model ARNs have an empty account field. skip this and calls fail. fn.addToRolePolicy( new iam.PolicyStatement({ actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], resources: ['us-east-1', 'us-east-2', 'us-west-2'].flatMap((r) => [ `arn:aws:bedrock:${r}::foundation-model/${MODEL.replace(/^(us|eu|apac|global)\./, '')}`, `arn:aws:bedrock:${r}::foundation-model/${FALLBACK_MODEL.replace(/^(us|eu|apac|global)\./, '')}`, ]), }), ); const url = fn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE }); new cdk.CfnOutput(this, 'FunctionUrl', { value: url.url }); new cdk.CfnOutput(this, 'WebhookUrl', { value: `${url.url}api/v1/github_webhooks`, }); new cdk.CfnOutput(this, 'LogGroup', { value: fn.logGroup.logGroupName }); } } Enter fullscreen mode Exit fullscreen mode cdk init also generates bin/pr-agent-infra.ts with the env block commented out, which makes the stack region-agnostic. The region list in the second policy statement isn't, so set the region explicitly: new PrAgentInfraStack(app, 'PrAgentInfraStack', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' }, }); Enter fullscreen mode Exit fullscreen mode Why two IAM statements Sonnet 4.5 can only be called through a cross-region inference profile (the us. prefix), which sends each request to one of a few US regions. AWS requires permissions on both the profile and the model in each of those regions, so the stack has two statements. The region list above works for us-east-1, us-east-2 and us-west-2. In Europe, use the eu. prefix and your EU regions, and you can check the exact list with aws bedrock get-inference-profile. For a simpler setup, you can use a model that runs in your own region, like DeepSeek V3.2 below. Using a different model DeepSeek V3.2 runs in your own region, and it doesn't need the use case form or a Marketplace subscription. Here I use it as both the main model and the fallback, and the region list only needs your region: const MODEL = 'deepseek.v3.2'; const FALLBACK_MODEL = 'deepseek.v3.2'; // and in the second policy statement: ['us-east-1'] Enter fullscreen mode Exit fullscreen mode The first policy statement will then grant access to an inference profile that doesn't exist, which is harmless. aws bedrock list-foundation-models shows what's available in your region, but not what your account is allowed to call, so the first real call is what tells you. 5. Deploy npx cdk bootstrap # once per account/region npx cdk deploy Enter fullscreen mode Exit fullscreen mode The server has a health check at /, so you can check that the image, the Function URL and the cold start all work before connecting GitHub: curl -i "$(aws cloudformation describe-stacks --stack-name PrAgentInfraStack \ --query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" \ --output text --region us-east-1)" Enter fullscreen mode Exit fullscreen mode You should get {"status":"ok"}. If you call the WebhookUrl output instead, you'll get a 405, because that path only accepts POST. A healthy response doesn't mean the secret was found. Tail the logs and look for Applied AWS Secrets Manager configuration on cold start. If you see Failed to get secrets from AWS Secrets Manager instead, the secret is in another region or has a different name: aws logs tail "$(aws cloudformation describe-stacks --stack-name PrAgentInfraStack \ --query "Stacks[0].Outputs[?OutputKey=='LogGroup'].OutputValue" \ --output text --region us-east-1)" --follow Enter fullscreen mode Exit fullscreen mode 6. Connect the webhook Take the WebhookUrl output and paste it into your GitHub App's webhook settings instead of example.com. Keep the same secret. Then open a test PR. The App's "Recent Deliveries" tab shows what happened. Timed out is expected, as we saw earlier. A 403 means the signature check failed, and the response body tells you why: Request signatures didn't match! means the secret in the App is different, and Webhook secret not configured means the secret from step 2 wasn't found. For anything else, check the logs. Cleaning up cdk destroy only removes what CDK created, so the secret and the ECR repo need to be deleted separately: npx cdk destroy aws ecr delete-repository --repository-name pr-agent --force --region us-east-1 aws secretsmanager delete-secret --secret-id pr-agent/config \ --force-delete-without-recovery --region us-east-1 Enter fullscreen mode Exit fullscreen mode Then delete the GitHub App, or at least uninstall it, so it stops sending webhooks to a URL that doesn't exist anymore. Without --force-delete-without-recovery, the secret stays in a 30 day recovery window. You aren't billed for it, but you can't create a new secret with the same name until the window ends. What it costs Lambda is the cheap part. Its free tier includes 1 million requests a month, and enough compute time for over 3,000 one minute reviews with 2 GB of memory. After that, each review costs about a fifth of a cent. On top of that you pay $0.40 a month for the secret and about $0.10 per GB-month for the image in ECR. Most of the cost is the model, and it depends on how big your PRs are, so I'd run it for a week on a real repo and check the Bedrock pricing page before estimating. Where to go from here PR-Agent is community owned and we're always looking for contributors. If you deploy this and run into issues, feel free to open an issue, that feedback really helps the project. Need guidance or just want to talk? Feel free to reach out to me over LinkedIn, x or email me at me@naor.dev If you enjoyed this post and want to follow along with my open source journey, feel free to follow me here and on GitHub. Thanks for reading this!

来源报道 · 1 篇 · 以下链接均前往原始媒体

← 返回精选动态 原文

更多内容