Private Agent Factory(이하 PAF)에서 만든 Custom Flow는 A2A(Agent2Agent) 프로토콜을 사용하여 다른 에이전트로부터 이용할 수 있습니다. 특정 PAF에서 준비한 검색이나 업무 처리를 동일 PAF 상의 Custom Flow에서 호출하고 싶을 때, 다른 PAF의 Custom Flow에서 호출하고 싶을 때, 완전히 다른 에이전트 기반상의 에이전트로부터 호출하고 싶을 때 사용할 수 있는 메커니즘입니다.
이번에는 일본어 Wikipedia를 검색하는 작은 Custom Flow를 공개하고, 다른 Custom Flow의 서브 에이전트로 연결하는 구성을 소개합니다. 검증에는 동일한 PAF를 공개 측과 호출 측 양쪽에 사용했습니다.
이번 환경에서는 HTTPS 통신에 사용하는 TLS 인증서(일반적으로 SSL 인증서라고도 불립니다)에 자체 서명 인증서를 사용하고 있습니다. 따라서, 호출 측의 PAF가 해당 인증서를 신뢰할 수 있도록 인증서의 outbound trust store에 등록하는 것이 필요했습니다. 또한, 동일한 PAF 내에서 A2A의 요청과 응답을 처리하기 때문에 여러 워커로 실행 설정하는 것도 필요했습니다.
애초에 이 검증을 하게 된 계기는 Tools로 분류된 컴포넌트 중에서도 ReAct Agent의 Tool로서 직접 연결할 수 없는 것들이 있었기 때문입니다. Wikipedia search도, 입력으로부터 검색하여 결과를 반환하는 Custom Flow로 만들면 A2A를 통해 처리를 위임할 수 있습니다. 이 방법을 사용하면 검색 처리를 공개 측에 두고 다른 Custom Flow에서 공유할 수 있습니다. 또한, 업무 데이터베이스 접근의 경우 특정 에이전트나 에이전트 기반에 데이터베이스 접근을 가두고(close) 호출 측은 필요한 결과만 받아 데이터베이스에는 건드리지 않도록 하는 목적에도 사용할 수 있습니다.
이번 구성은 다음과 같습니다. 그림은 연결 관계를 보여줍니다.
공개 측은 검색 키워드를 받아서, 기사의 개요와 URL을 반환합니다. LLM은 호출하지 않습니다. Wikipedia Search Agent라는 이름이지만, AI 에이전트가 아니라 단순히 받은 검색 키워드로 Wikipedia를 검색하여 결과를 반환할 뿐입니다 질문의 해석, 검색 키워드 선택, 얻은 정보로부터 답변 작성은 호출 측의 Agent가 담당합니다.
A2A에서는 에이전트의 이름이나 기능, 연결처, 인증 방식을 기재한 Agent Card를 가져와 연결처를 확인합니다. PAF에서는 이를 External agents에 등록하고, Custom Flow 상의 External Agent 컴포넌트에서 이용합니다. 공식 A2A 가이드에도 공개 측과 호출 측의 절차가 준비되어 있습니다.
대상 환경은 다음과 같습니다.
| 항목 | 이번 환경 |
|---|
| PAF | 26.7 (컨테이너 이미지 태그 26.7.0.0.0 ) |
| ... | |
PAF의 구축은 완료된 전제입니다. 명령어는 PAF를 구동하는 Linux 호스트의 Bash에서 실행합니다. 로그인에는 해당 환경에서 PAF의 컨테이너를 관리하는 OS 사용자(Marketplace에서 배포한 경우, opc 사용)를 이용합니다. <PUBLIC_IP> , <STAGING_DIR> , <DB_USER> , <AGENT_ID> 는 꺽쇠(<>) 그대로 각 환경의 값으로 대체해 주십시오. | |
다른 PAF에서 연결하는 경우에도 공개와 등록 과정은 공통입니다. 그 경우에는 호출 측에서 공개 측의 Agent Card와 A2A 엔드포인트에 도달할 수 있어야 합니다. 이번 단일 워커로 인한 만남(待ち合わせ) 문제는 동일한 PAF에 연결했을 때 확인된 것입니다.
Agent Builder에서 Custom Flow를 만들고 이름을 Wikipedia Search Agent로 했습니다. Custom Flow의 Description은 wikipedia_search_agent로 설정했습니다.
제가 시도해 본 한도 내에서는 Custom Flow의 Description에 기재가 길어지면 호출에 실패하는 경우가 있었습니다. 현재로서는 Description은 짧은 이름으로 유지하는 것이 추천됩니다.
컴포넌트 설정은 다음과 같습니다. Chat input과 Chat output은 포트 연결 외에는 이 예시 고유의 설정이 없습니다.
| 컴포넌트 | 항목 | 설정값 |
|---|
| Chat input | 입력 | 실행 시 검색 키워드를 입력 |
| ... | | |
| 연결하는 포트는 2쌍입니다. | | |
| 연결 출처 | 연결 목적지 |
|---|
| Chat input: Message | Wikipedia search: Search Query |
| Wikipedia search: Message | Chat output: Message |
Wikipedia search에는 JSON과 Message의 출력이 있습니다. 이 예시에서는 사람이 읽을 수 있는 형태로 정리된 Message를 사용합니다. Number of Results는 검색 결과 기사 수를 의미하며, 기본값은 4이고 설정 범위는 1~10입니다. 이는 한 기사에서 추출하는 단락 수가 아닙니다. 컴포넌트 공식 설명
저장한 후, Playground에서 검색 키워드를 입력합니다. 확인해야 할 것은 기사 제목, 개요(summary), URL이 반환되는 것입니다.
이 Custom Flow가 주로 가져오는 것은 기사 초반의 개요입니다. 이번에 조사한 PAF 26.7 구현에서는 검색 결과마다 summary API의 extract를 가져왔습니다. 개요 가져오기에 실패했을 경우 등에는 검색 일치 지점의 짧은 발췌본으로 전환되며, URL이 비어 있는 경우도 있습니다.
따라서 기사 본문의 '역사' 등에 쓰인 정보까지 답변할 수 있다고는 할 수 없습니다. Number of Results를 늘린다고 해서 같은 기사를 깊게 읽는 동작이 되는 것은 아닙니다. 이 블로그에서는 기사 본문을 가져오기 위한 추가 처리는 다루지 않지만, REST API Tool로 Wikipedia의 API를 호출하면 쉽게 구현할 수 있을 것 같습니다.
공개 측 Custom Flow를 저장하고 Publish한 후, 채팅 화면을 엽니다. Integration options → A2A에서 A2A access를 활성화하고 Agent Card URL을 복사합니다. 이어서, 이 Custom Flow 전용의 A2A API key를 생성합니다.
Agent Card URL은 다음 형태입니다.
Publish만으로는 A2A 연결이 활성화되지 않습니다. 공개 조건과 활성화 절차
API key는 나중에 Bearer token으로 등록합니다. URL에 임베드하지 마세요.
첫 번째 discovery는 다음 오류로 실패했습니다.
CERTIFICATE_VERIFY_FAILED: self-signed certificate
브라우저에서 PAF 화면을 열고 있어도, PAF 자체가 HTTPS 연결 대상을 신뢰할 수 있는 것은 아닙니다. 브라우저의 인증서 설정과, PAF가 외부 통신에 사용하는 outbound trust store는 별개입니다. 이번 환경에서는, 자신이 배포하는 자체 서명 인증서도 이 신뢰 스토어에 등록해야 했습니다.
등록에는 공식 문서의 관리자 API를 사용합니다. 공식 문서는 CA 및 중간 CA 인증서 추가를 설명하고 있으며, 다음 자체 서명 서버 인증서를 사용하는 예시는 이번 검증을 기반으로 합니다. 기존 신뢰 스토어에서 검증 가능한 연결 대상이라면, 이 추가 작업은 필요하지 않습니다.
동일 환경에 연결하므로, 호스트에 있는 공개 인증서를 사용합니다.
BASE_URL='https://<PUBLIC_IP>:8080'
CERT_FILE='<STAGING_DIR>/applied-ai/volume/config/app/latest/certs/cert.pem'
openssl x509 -in "$CERT_FILE" \
...
$ openssl x509 -in "$CERT_FILE" \
> -noout -subject -issuer -dates -fingerprint -sha256 -ext subjectAltName
subject=CN = agentfactoryvm.subxxxxxxxxxx.ocigenerativeai.oraclevcn.com
...
IP 주소로 연결하는 경우, 인증서의 SAN(Subject Alternative Name)에 해당 IP가 포함되어 있는지 확인합니다.
openssl x509 -in "$CERT_FILE" -noout -checkip '<PUBLIC_IP>'
이번에는 CN(Common Name)이 내부 FQDN 그대로였지만, SAN에 연결지 IP가 있어 IP 일치 검사에 성공했습니다.
FQDN이나 IP 주소를 지정하여 자체 서명 인증서를 재작성하려면 Generate Your Own SSL Certificates를 참조해 주세요.
outbound trust store에 등록하는 것은 cert.pem입니다. 비밀키인 key.pem은 사용하지 않습니다. 다른 PAF(Private Agent Factory)에 연결할 경우에는, 그 연결 대상을 검증하기 위한 인증서를 호출 측에 등록합니다.
인증서 등록에는 PAF의 관리자 API를 사용합니다. 이 API를 호출하려면 관리자로 로그인했음을 나타내는 인증 정보가 필요합니다. 먼저 curl로 로그인하여 PAF가 반환하는 세션 Cookie를 얻습니다. 그 Cookie를 인증서 등록 요청에 첨부하여 보내면, 관리자로서 등록을 수행할 수 있습니다.
아래 명령어에서는 PAF의 관리자 계정으로 로그인하고, 획득한 Cookie 값을 변수 AGENT_FACTORY_SESSION에 저장합니다. 이전에 발행했던 A2A API key는 여기서는 사용하지 않습니다.
전체 처리를 { ... }로 감싸서 한 번에 붙여넣을 수 있도록 했습니다. Bash는 닫는 중괄호까지 읽은 후에 실행하므로, 중간의 read가 붙여넣은 코드를 입력으로 읽어들이는 것을 방지할 수 있습니다. 마지막 }까지 붙여넣고 필요하다면 Enter를 누르세요.
{
read -r -p 'Agent Factory 관리자 이메일 주소: ' ADMIN_EMAIL
AGENT_FACTORY_SESSION="$(
...
첫 번째 입력 요청에 관리자 이메일 주소를 입력하고, 이어지는 curl의 입력 요청에 관리자 비밀번호를 입력합니다. 세션을 획득하지 못하면 다음으로 진행하기 전에 인증 정보를 확인해 주세요.
직전 로그인 명령어에서는 curl이 HTTPS로 PAF에 연결했습니다. 그 안의 --cacert "$CERT_FILE"은 '연결 대상을 확인할 때, 손안의 cert.pem을 신뢰하는 인증서로 사용'하라는 curl에 대한 지정입니다. 이 지정은 해당 curl 명령어에 의한 통신에서만 유효합니다. PAF 자체가 A2A 연결에 사용하는 신뢰 저장소에는 아직 등록되지 않았습니다.
다음 명령어로 인증서를 PAF의 신뢰 저장소에 등록합니다. 여기에서도 --cacert는 curl의 연결 시 확인에 사용됩니다. 인증서 파일을 PAF로 보내는 것은 마지막의 `--form
이 작업은 Private Agent Factory를 1 워커로 구동할 때만 필요합니다.
워커 수는 PAF가 동작하는 Linux 환경에서 다음 명령어로 프로세스 수를 확인하여 알 수 있습니다.
podman exec oracle-applied-ai-label bash -c \
'ps -eo pid,ppid,args | grep "[g]unicorn_config/uvicorn_config.py"'
부모 프로세스 1줄과 그 PID를 부모로 가지는 워커가 보일 것입니다.
다음과 같이 표시되면 1 워커로 구동 중입니다.
$ podman exec oracle-applied-ai-label bash -c \
> 'ps -eo pid,ppid,args | grep "[g]unicorn_config/uvicorn_config.py"'
306 1 /home/aaiuser/install/agent_factory/third_party/python3/bin/python3.12 /home/aaiuser/install/agent_factory/third_party/python3/bin/gunicorn --config /home/aaiuser/install/agent_factory/gunicorn_config/uvicorn_config.py
...
인스톨러에서 DB 연결 테스트를 수행하기 전에 Oracle AQ의 설정을 완료하지 않으면 이 상태가 됩니다.
제 환경에서는 인증서를 등록했음에도 불구하고, discovery 화면에는 동일한 오류가 표시되었습니다. 하지만 서버 로그 내용은 바뀌어 있었습니다.
로그 파일은 <STAGING_DIR>/applied-ai/volume/log/app/latest/log/agent_factory.log 였습니다.
<STAGING_DIR>는 Marketplace에서 배포하여 한 번도 업그레이드하지 않았다면 /u01/staging 입니다.
TimeoutError: The read operation timed out
이번 PAF는 1 워커로 동작하고 있었습니다. 구현을 조사해 보니, discovery는 자신의 Agent Card를 동기 HTTP 호출로 가져오는 구조였습니다. 응답을 기다리는 동안 워커의 이벤트 루프가 멈추면서, 같은 워커에서 처리해야 할 Agent Card 요청이 진행되지 못했습니다.
독립된 진단 프로세스에서는 올바른 트러스트 스토어와 키를 사용하여 약 1.5초 만에 Agent Card를 가져올 수 있었습니다. 반면, UI를 통한 자체 연결은 타임아웃되었고, 4 워커로 변경한 후에 성공했습니다. 코드 구조와 로그의 시간 관계까지 고려했을 때, 단일 워커에서의 대기(waiting)가 원인이라고 판단합니다.
이 대기를 피하기 위해서는 요청을 기다리는 처리와 응답하는 처리를 다른 워커에서 받을 수 있도록 합니다. 아이디어는 2개 이상이지만, 이번에 동작을 확인한 설정은 4 워커입니다. 워커를 늘리는 방법은 동기 호출 자체의 수정이 아니므로, 동시에 사용량이 증가할 경우에는 별도로 동작을 확인해 주십시오.
다중 워커화는 공식 스케일링 절차를 따릅니다. 먼저, PAF가 있는 DB/PDB에 DB 관리자 자격으로 접속합니다. Private Agent Factory 화면의 관리자나 Private Agent Factory 전용 리포지토리 사용자(repository user)가 아니라, 데이터베이스의 관리자입니다.
공식 페이지의 Configure Oracle AQ에 있는 SQL 전체를 사용하고, <DB_USER>를 리포지토리 사용자 이름으로 대체하여 실행합니다. 이 SQL에는 권한 부여, 큐 테이블 및 큐 생성, 시작, 구독자(subscriber) 추가가 포함됩니다. 예상치 못한 오류가 발생하면 재시작하기 전에 DB 설정을 확인합니다.
아래는 구성을 확인할 SQL입니다. 일반적으로 대문자 사용자 이름을 사용하는 예입니다.
SELECT OWNER, NAME, QUEUE_TABLE, ENQUEUE_ENABLED, DEQUEUE_ENABLED
FROM DBA_QUEUES
WHERE OWNER = '<DB_USER>' AND NAME = 'AAI_SOCKETIO_QUEUE';
...
이번에 확인한 것은 AAI_SOCKETIO_QUEUE의 ENQUEUE와 DEQUEUE가 모두 YES인 상태였습니다.
、サブスクライバーが AAI_SIO_1
から AAI_SIO_8
までの8건이라는 상태입니다.
여기서부터 애플리케이션이 일시 정지합니다. 실행 중인 채팅이나 플로우가 없는 시간에, 호스트 측에서 다음을 실행합니다.
podman exec -u root oracle-applied-ai-label bash -c \
'export AGENT_FACTORY_ASGI_WORKERS=4; /home/aaiuser/install/agent_factory/manage_app.sh --all --restart'
이것은 검증 환경에서 성공한 명령어입니다. 2026년 10월 13일에 확인한 공식 페이지에서는 끝 부분이 manage_app.sh restart all라는 표기였습니다. 사용하고 있는 버전의 스크립트와 절차를 확인해 주세요.
재시작 시에는 도중에 대기 표시만으로 판단하지 말고, 끝까지 확인합니다.
async server waiting for workers | workers=0/4 ready=0/4
async server waiting for workers | workers=4/4 ready=0/4
async server ready | master_pid=<MASTER_PID> workers=4/4
...
프로세스 목록에서도 확인할 수 있습니다.
podman exec oracle-applied-ai-label bash -c \
'ps -eo pid,ppid,args | grep "[g]unicorn_config/uvicorn_config.py"'
부모 프로세스 1줄과, 그 PID를 부모로 가지는 워커 4줄이 있으면 4워커입니다. 총 5줄을 5워커로 세지 않도록 합니다.
위 명령어의 export는, 그때 실행되는 프로세스에 적용됩니다. 저장된 컨테이너 설정을 덮어쓰는 처리가 아닙니다.
이번 환경에서는 명시 지정이 없고, AQ 준비를 나타내는 마커 socketio_aq_ready.json도 없는 경우, 구동 처리는 1워커로 돌아가는 구조였습니다. DB에 AQ를 구성한 것과, 이 마커가 있다는 것은 별개입니다. 참고로, 이 마커 socketio_aq_ready.json는, 인스톨러에서 DB 연결 테스트를 진행했을 단계에서 이미 Oracle AQ가 세팅되어 있다고 자동으로 생성되는 것 같습니다(아직 시도해 보지 않았습니다).
설정 변경의 영속화에는 이 AQ 준비 마커 socketio_aq_ready.json을 만들어 주면 좋을 것 같습니다. 실제로 해봤더니 잘 되었습니다.
먼저, <STAGING_DIR>/applied-ai/volume/data/app/latest/markers/socketio_aq_ready.json 파일을 생성하고, 다음 내용을 기재합니다.
{
"ready": true,
"queue_name": "AAI_SOCKETIO_QUEUE",
...
}
그리고 required_workers는, 앞서의 Oracle AQ 설정에 맞춥니다.
<STAGING_DIR>에서, make restart를 실행하여 컨테이너를 재시작합니다.
재시작했으면 프로세스 목록을 확인합니다.
podman exec oracle-applied-ai-label bash -c \
'ps -eo pid,ppid,args | grep "[g]unicorn_config/uvicorn_config.py"'
부모 프로세스 1줄과, 그 PID를 부모로 가지는 워커 8줄이 있으면 8워커입니다.
AQ 준비 마커 socketio_aq_ready.json을 수동으로 생성하는 것은 공식 문서에는 기재되어 있지 않습니다. 향후 업그레이드에서 덮어쓰여질 가능성도 있으므로, 업그레이드 시에는 워커 수를 확인하는 것이 좋을 것입니다.
Private Agent Factory에서, Utilities → A2A Agents → External agents → Add external agent를 열고, 다음을 입력합니다.
항목 | 값 |
|---|---|
| Agent Card URL | 공개 측에서 복사한 URL |
| ... |
키 이름은 「API key」이지만, 인증 방식은 Bearer token입니다. 입력값에 Bearer
Discover agent를 누르고, 이름(Name), Description, Skills, Endpoint와 호환성 표시를 확인합니다. 인증서 등록과 워커 수를 늘린 후에 다음 화면까지 진행했습니다.
이 화면에서는 Wikipedia Search Agent가 감지되었고, External agent is compatible.라고 표시되어 있습니다. Description과 Skills의 설명은 wikipedia_search_agent이며, 통신 방식은 JSON-RPC입니다. IP 주소와 ID는 마스킹 처리되었습니다.
검증으로 얻은 Agent Card는 A2A 1.0의 JSON-RPC 연결을 보여주었으며, 처리 전송지는 다음과 같은 형태였습니다.
이 화면 왼쪽 하단의 Add external agent에서 등록하고, Enable을 실행하여 Connected 상태가 되는지 확인합니다. 등록 직후에는 Disabled입니다. 감지 성공 화면만으로는 활성화나 실행 확인은 완료되지 않았습니다. 등록과 상태의 공식 문서를 참고하세요.
호출 측에서는 다른 Custom Flow로 Wikipedia Agent를 만듭니다. Description은 예를 들어 「Wikipedia Search Agent에 검색을 위임하고, 얻은 기사 개요를 기반으로 답변한다」와 같이 작성합니다. 이 섹션의 값은 실행 확인에 사용된 설정 예시입니다.
| 컴포넌트 | 항목 | 설정 예시 |
|---|
| Agent | LLM | LLM Management에 등록된, tool calling을 지원하는 모델 |
| ... | | |
| 연결원 | 연결처 | |
| --- | --- | |
| Chat input:Message | Agent:Prompt | |
| ... | | |
서브 에이전트로 사용할 경우, External Agent의 Input은 이 위임에는 사용되지 않습니다. Agent가 위임 시 메시지를 만들어서 전달합니다 (External Agent 공식 설명). 이때, Agent는 External Agent가 LLM이라는 두뇌를 가지고 자연어로 지시를 받아들일 수 있는 것으로 가정하고 메시지를 구성하는 것 같습니다. 검색 키워드만 전달하도록 지정하지 않으면, 질문문이나 작업 지시를 전달할 수 있습니다. 이번에는 위임 대상이 LLM을 가지지 않은 단순한 플로우였기 때문에, 자연문을 전달하지 않도록 Agent의 Custom instructions에 프롬프트(후술)를 기재합니다.
이번에 시도해 본 바로는, 공개 측 Custom Flow의 Description이 호출 측 External Agent의 Agent description에 설정됩니다 (수정해도 덮어쓰여집니다). 이번 PAF 26.7의 동작을 조사한 바에 따르면, 이 Agent description은 내부 서브 에이전트 이름에도 사용되고 있었습니다.
긴 설명문을 설정했을 때는 LLM이 긴 Description을 유용하여 대상명에 포함되어 있던 「복수 지정」이라는 문구를 동의어인 「다수 지정」으로 바꿔버려 이름의 완전 일치에 실패하고 호출할 수 없는 예가 있었습니다. 이 단계에서는 A2A의 HTTP 통신까지 진행하지 않았습니다.
그래서, 이번 예시에서는 wikipedia_search_agent라는 짧은 값을 사용하고, 자세한 이용 조건은 부모 Agent의 Custom instructions에 작성합니다.
Agent의 Custom instructions 프롬프트 예시는 다음과 같습니다.
## 태스크
- 사용자 질문을 이해하여, 질문에 답변하는 데 필요한 사항(1개 또는 복수)을 특정합니다.
- 특정된 모든 사항을 wikipedia에서 조사합니다.
...
저장 후, Playground의 새 채팅창에서 예를 들어 다음 질문을 입력합니다.
이번에는 화면에 어느 원인으로든 같은 메시지가 나타났습니다.
External agent could not be discovered. Check the Agent Card URL and try again.
원인을 분리하기 위해서는, 작업을 다시 시도한 직후의 로그가 도움이 되었습니다.
| ⚠️ 로그/증상 | 이번에 확인한 원인과 대응 |
|---|
CERTIFICATE_VERIFY_FAILED: self-signed certificate | 아웃바운드 트러스트 스토어(outbound trust store)에 공개 인증서 추가 |
인증서 등록 후 The read operation timed out | |
| 동일 환경으로의 동기 연결이 1 워커에서 대기. AQ 구성 후 4 워커화 | |
| 재부팅 후 타임아웃 재발 | 실제 워커 수와 영구 설정을 확인 |
호스트상의 주요 로그는 다음 위치에 있습니다.
<STAGING_DIR>/applied-ai/volume/log/app/latest/log/agent_factory.log
<STAGING_DIR>/applied-ai/volume/log/app/latest/log/agent_factory_asgi.log
<STAGING_DIR>/applied-ai/volume/log/app/latest/nginx/access.log
HTTP 상태 코드에도 주의가 필요합니다. 이번에 인증되지 않은 접근은 로그인 화면으로 전달되었고, 그 HTML이 200을 반환했습니다. Agent Card를 가져올 때는 JSON이 반환되며, 카드 항목이 포함되는지까지 확인해야 합니다.