从像素到战场:红外场景仿真引擎的核心技术深度解析
掘金
从像素到战场:红外场景仿真引擎的核心技术深度解析 摘要 红外场景仿真(Infrared Scene Simulation),是通过计算机在虚拟环境里"合成"一台红外探测相机的成像结果的技术。
Балл: 57.15Уверенность: 54%
ПодробнееЗагружаем каталог…
НАВИГАТОР ПО ВОЗМОЖНОСТЯМ ИИ
Найдите свой ИИ-инструмент. Бесплатный доступ, пробные периоды и кредиты — в одном месте.
掘金
从像素到战场:红外场景仿真引擎的核心技术深度解析 摘要 红外场景仿真(Infrared Scene Simulation),是通过计算机在虚拟环境里"合成"一台红外探测相机的成像结果的技术。
Балл: 57.15Уверенность: 54%
Подробнее人人都是产品经理
一家企业做新品开发,要管 34 个节点、4 个阶段,现在靠 Excel 和群里问进度。客户想要一个看板:节点可折叠、自动算逾期,交付物能上传预览写意见,采购单还要从前面下推。采购订单、审批流、库存记账这些 ERP 都有,缺的是进度和跨节点归档这两层。 前阵子客户提了个需求。一家企业做新品开发,从打样到入库 34 个节点、4 个阶段。现在用 Excel 管,每个节点记着计划起止日期、应交交付物、实际完成日期,谁交了、卡在哪一步靠群里问。 他们想要个页面:节点按阶段分组可折叠,自动显示状态和逾期天数;交付物能上传、预览、下载、写意见;节点 29 把前 28 个节点的资料打包归档;节点 30、31 建采购订单,33、34 做采购到货和入库,后面的单子从前面下推,不允许凭空新增。 这里面一半 ERP 有,另一半 ERP 没这个概念。 采购订单、采购到货单、采购入库单是现成的,审批流、下推转换、单据编码、库存记账也是标准能力,不用开发。 ERP 没有的是“进度”。ERP 管单据,单据状态只有未审核、已审核。“这个节点该交什么、交没交、超期几天”不是单据状态,是按计划日期和实际情况算出来的。ERP 里没有节点这个对象,也没有阶段、完成率、逾期数。即使是YonSuite的项目云或专业的项目管理软件也没这种一个页面概览所有节点的功能。 交付物也不是单据。ERP 的附件挂在单据上、跟着签审走,这里的附件挂在节点上,一个节点可能好几份、格式各异,还要在线预览和写意见。 还有几处细节。当天算不算超期,计划结束空的怎么算,上传就算完成还是确认才算,删了要不要退回,这几条组合起来才能得出一个状态,ERP 不会预置。归档同理,把跨节点的资料打成一个包是批处理动作,ERP 的附件按单据存,没有跨对象打包这回事。 另外,ERP 有下推,但标准的单据入口允许手工新增。而客户要求到货单和入库单不允许无源新增,而且只能在看板这个页面弹框操作,不喜欢跳转到ERP原生的《采购订单》、《采购入库单》等页面。 单据能复用,进度和资料要单独做一层。这种 需求难点 是 在ERP的一个页面综合多条个性化需求+原生功能模块(嵌入式) ,从市面上的大erp来看,没有一个能实现,无论是用友ys还是金蝶星空或所谓ai套件,传统开发的难度也很大,咋办呢,头疼。 作者:业财老曾 公众号:业财老曾谈 本文由 @业财老曾 原创发布于人人都是产品经理。未经作者许可,禁止转载 题图来自 Unsplash,基于CC0协议
Балл: 57.01Уверенность: 54%
ПодробнееiThome 新聞
衛福部健康幣10月1日正式上路,要把民眾健檢、癌篩、打疫苗等預防保健行為,轉換為可查詢、可兌換的數位獎勵。年滿18歲且持有臺灣健保卡的民眾,完成健保快易通App身分驗證並綁定國健署Health GO App後,就能透過健康存摺的健康幣專區,來查詢累積情形。今年12月1日起,累積滿1,000健康幣,民眾可依規定於合作通路兌換健康商品、電子票券或折抵部分自費醫療項目。 健康幣10月1日上路,健保快易通身兼點數查詢入口 做完健檢、癌篩或打疫苗後,能不能不用自己留單據、拍照上傳,也能自動拿到獎勵?
Балл: 56.48Уверенность: 54%
Подробнееvelog
Dubai is a place where impressive vehicles are a familiar sight on the road. Wide highways, modern attractions, luxury hotels, stylish restaurants, and vibrant entertainment areas create an ideal setting for enjoying a premium car. Among the vehicles that attract attention, Brabus rental Dubai and Ferrari SF90 stand out for very different reasons. A Brabus offers a bold combination of power, luxury, and commanding road presence, while the Ferrari SF90 delivers an exciting sports car experience with remarkable performance. If you are considering renting either vehicle in Dubai, there are several important things to understand before making a booking. From choosing the right car for your plans to checking rental conditions, insurance, mileage, deposits, and driving requirements, careful preparation can make the entire experience more enjoyable. Understand What a Brabus Rental Offers Brabus is known for creating highly customized versions of premium Mercedes vehicles. Depending on the particular model available from a rental company, a Brabus can provide an impressive combination of luxury, performance, distinctive styling, and a carefully finished interior. A Brabus SUV can be particularly practical for Dubai because it combines a spacious cabin with a strong road presence. It can be used for business meetings, hotel transfers, shopping trips, sightseeing, and special occasions. Before booking, ask the rental company about the exact Brabus model. Different Brabus vehicles can have very different specifications, seating arrangements, engine performance, and equipment. Knowing the exact model helps you choose a vehicle that matches your plans. Explore the Ferrari SF90 Experience The Ferrari SF90 provides a completely different type of driving experience. It is a high performance Ferrari designed for drivers who want an exciting sports car with advanced technology and impressive acceleration. Its low profile and sports car design make it especially suited to travelers who want a memorable vehicle for special occasions or leisure drives. The Ferrari SF90 can be an attractive option for photography sessions, celebrations, luxury outings, and enjoying Dubai's modern roads. However, a sports car requires more consideration than a large SUV. Limited luggage space, low ground clearance, and a different seating arrangement can affect where and how you use the vehicle. Consider your itinerary carefully before choosing the SF90 for an entire trip. Think About Your Travel Plans Your schedule should play an important role in deciding between a Brabus and Ferrari SF90. If you need a vehicle for several passengers, luggage, business appointments, and longer journeys, a Brabus may provide greater practicality. If you are traveling alone or with one passenger and your main objective is to enjoy a high performance sports car, the Ferrari SF90 may better match your plans. Some visitors may even choose different vehicles for different occasions. A spacious Brabus can handle daily travel, while the Ferrari SF90 can be reserved for a special evening, celebration, or memorable drive. Check the Rental Price and Inclusions Never look only at the headline rental price. Before confirming your booking, ask exactly what the quoted amount includes. Important details can include insurance, mileage allowance, taxes, delivery and collection charges, additional driver fees, and other rental costs. Premium and high performance vehicles can have specific conditions that differ from standard car rentals. Ask whether the quoted rate is calculated by day, several days, or another rental period. If you intend to keep the car for an extended stay, ask about longer rental packages. A clear understanding of the total cost allows you to plan your budget properly. Understand the Security Deposit Luxury and performance cars may require a security deposit. Before collecting the vehicle, confirm the required amount and payment method. Ask how the deposit is handled and what circumstances can affect its return. Traffic fines, toll charges, fuel differences, or vehicle damage may be handled according to the terms of the rental agreement. Reading this section carefully can prevent misunderstandings later. If anything in the agreement is unclear, ask the rental company for an explanation before signing. Inspect the Vehicle Carefully A detailed inspection is especially important when renting an expensive vehicle. Before driving away, examine the exterior and interior carefully. Look for scratches, dents, wheel marks, paint damage, glass damage, and any other visible imperfections. Inside the vehicle, check the seats, dashboard, displays, controls, and other equipment. Make sure existing damage is documented by the rental company. Taking photographs or videos at the beginning of the rental can also provide a useful record of the vehicle's condition. For a Ferrari SF90 or a customized Brabus, careful inspection is particularly sensible because repairs and cosmetic components can be costly. Understand Mileage Restrictions Mileage conditions can have a major effect on your rental experience. Some rental agreements provide a daily mileage allowance, while additional distance may result in extra charges. Think about where you plan to drive before selecting a rental package. If you expect to visit different parts of Dubai or travel to another emirate, your mileage requirements may be higher. Ask the rental provider how additional mileage is calculated. Knowing this before booking can help you avoid unexpected expenses when returning the vehicle. Learn About Dubai Roads Dubai has an extensive network of modern roads, but traffic conditions can change considerably throughout the day. Drivers should pay attention to speed limits, road signs, lane markings, traffic signals, and other instructions. If you are driving a Ferrari SF90, extra attention is useful because its performance can make it tempting to focus on acceleration rather than normal road conditions. Always follow posted speed limits and local traffic regulations. A Brabus may feel larger than the vehicles you normally drive, particularly when parking or moving through tighter areas. Take time to become familiar with the vehicle before entering busy traffic. Consider Parking and Ground Clearance Parking should be part of your planning when renting a premium vehicle. Dubai has many large shopping centers, hotels, restaurants, and attractions with parking facilities, but individual spaces and entrances can vary. The Ferrari SF90 has a low sports car profile, so drivers should be cautious around ramps, raised surfaces, and other areas where ground clearance may become an issue. A larger Brabus can offer a more comfortable experience for general parking and passenger access, but its size should still be considered in compact parking areas. Follow the Fuel and Return Policy Ask about the fuel policy before beginning your rental. Find out whether the vehicle needs to be returned with the same fuel level as provided at collection. Also confirm the exact return location and time. If the vehicle is being delivered to your hotel or another location, make sure the collection arrangements are clearly understood. For premium cars, the return inspection can be detailed, so keeping the vehicle in good condition throughout the rental period is important. Choose the Vehicle That Fits Your Occasion A Brabus and Ferrari SF90 can both create an unforgettable driving experience, but they serve different purposes. A Brabus can be suitable for travelers who want luxury, strong performance, passenger space, and everyday usability. It can work well for business appointments, family travel, shopping, hotel visits, and longer drives. The Ferrari SF90 is more focused on the sports car experience. Its distinctive design and performance make it suitable for special occasions, leisure drives, celebrations, and travelers who want something truly different from an everyday vehicle. The right choice depends on your passengers, luggage, itinerary, driving preferences, and rental budget. Prepare Before You Collect the Car Before arriving at the rental location, make sure you have all required documents and understand the company's rental requirements. Confirm the accepted driving license, identification documents, payment arrangements, deposit, insurance terms, and rental duration. Take time to read the agreement instead of rushing through the collection process. Pay particular attention to mileage, traffic fines, toll charges, fuel requirements, insurance conditions, and vehicle return procedures. Once these details are clear, you can start your Dubai journey with fewer concerns. Enjoy Your Dubai Drive Renting a Brabus or Ferrari SF90 rental Dubai can turn ordinary transportation into a memorable part of your trip. The Brabus offers a spacious and commanding environment, while the Ferrari SF90 brings the character of a high performance sports car to the road. The most important step is choosing the vehicle according to your actual plans. Consider how many people will travel with you, where you will drive, how much luggage you have, and what type of experience you want. By checking rental terms, inspecting the vehicle, understanding mileage and deposit requirements, and following Dubai road regulations, you can make better use of your chosen vehicle. Whether you select the refined practicality of a Brabus or the thrilling character of a Ferrari SF90, proper preparation can make your time behind the wheel an enjoyable part of your Dubai visit.
velog
P/E Ratio: The One Formula Every Stock Screener Runs (and Gets Wrong) By Marketcaplens. All figures below are illustrative hypotheticals, not data from any real company. If you've ever built a stock screener, a watchlist, or a ranking page, you've computed this number: price divided by earnings per share. The P/E ratio is the most quoted valuation shortcut in markets — and the easiest to misuse. This is the formula behind it, the traps baked into it, and how to use it without fooling yourself. The two formulas P/E asks one question: how many dollars does the market pay for one dollar of a company's earnings? P/E = share price ÷ EPS Implied share price = EPS × P/E multiple The first is what the market is paying . The second is what the stock would be worth if you chose the multiple. Same relationship, solved for a different unknown. Take an illustrative example: a company earns $5 a share and trades at $100 . Its P/E is 20 . Run it through the P/E and PEG calculator and you also get the earnings yield — EPS ÷ price — which is 5% here, plus PEG if you add a growth rate. Turn it around with the P/E valuation calculator : $5 of EPS at a 15× multiple implies $75 a share; at 20× it implies $100 ; at 25× it implies $125 . If the stock actually trades at $90 , those cases sit below, above, and further above the market price. That gap — implied versus actual — is where the real question lives. The denominator is a label, not a number Every screener bug I've seen in P/E logic comes from the denominator. "EPS" is not one number. It's a period you have to name: Trailing — the last reported twelve months (or the last fiscal year, if that's all you have). Fiscal year — the last completed fiscal year, even if a newer quarter exists. Forward — an estimate for a future year. A forecast, not a reported fact. A $100 price over $5 of trailing EPS is a trailing 20×. The same price over $6 of expected next-year EPS is a forward 16.7×. Those are different questions. If your screener mixes trailing prices with forward earnings without labeling the period, the "cheap" stocks it surfaces are unit errors, not bargains. PEG has the same trap: it only makes sense when the P/E period and the growth period match — trailing earnings with trailing growth, or forward with forward. The plain-English P/E guide works through exactly this. What P/E tells you, and what it leaves out P/E compresses price and earnings into one multiple so you can compare companies. A 12× stock is cheaper on earnings than a 30× stock, all else equal. All else is rarely equal. A high multiple often means the market is paying up for growth. It can also mean earnings have collapsed while the price hasn't caught up yet — the ratio looks expensive for the worst possible reason. Read it next to the business, never as a buy or sell signal. And remember P/E only sees equity and earnings, not debt or cash. Two companies can share a P/E and have nothing else in common. Zero and negative EPS: undefined, not zero You cannot divide by zero, so P/E is undefined when EPS is exactly zero. A loss-making company produces a negative mathematical quotient, but that figure is not meaningful (N/M) for the usual comparison. Taking the absolute value, or flipping the sign so a loss looks like a bargain multiple, is a trick — not a valuation. Any screener that displays a negative P/E as a sortable number is lying to its users; blank is the honest output. Until earnings turn positive, use another lens: sales, cash flow, or a DCF model if you have free cash flow. The takeaway for builders P/E is two inputs and one division. The work is everything around it: labeling the EPS period, refusing to compute on zero or negative earnings, and presenting the implied-price cases (low, base, high) instead of a single magic number. Get those right and the ratio does what it was built to do — compress price and earnings into one comparable multiple. Get them wrong and your screener manufactures bargains that don't exist. For general education only. Nothing here is investment advice. More calculators: all MarketCapLens tools .
velog
이번주에는 면접관이 되었다. 이번 장의 주제는 무려 유튜브 설계 ... ! 문제 이해 및 설계 범위 확정 이번 장에서 다루고자하는 서비스 설계의 특징은 이러하다. • 빠른 비디오 업로드 • 원활한 비디오재생 • 재생 품질 선택 기능 • 낮은 인프라 비용(infrastructure cost) • 높은 가용성과 규모확장성, 그리고안정성 • 지원 클라이언트: 모바일 앱, 웹브라우저, 그리고 스마트 TV 개략적 규모 추정 CDN으로 비디오를 서비스하면 비용이 크다. 이 단계에서는 규모를 먼저 추정하고, 비용 절감 방법은 뒤에서 다룬다. 가정: DAU 500만 / 1인당 하루 5개 시청 / 사용자의 10%가 하루 1개 업로드 / 영상 평균 300MB 저장 용량: 500만 × 10% × 300MB = 하루 150TB 증가 CDN 비용: 500만 × 5개 × 0.3GB × $0.02 = 하루 $150,000 (CloudFront, 미국 트래픽 기준, 스트리밍 비용만 계산) 개략적 설계안 제시 및 동의 구하기 주어진 조건에 따라 기존 클라우드 서비스를 이용한다. 규모 확장이 쉬운 BLOB 저장소나 CDN을 직접 만드는 것은 매우 복잡하고 비용도 많이 들어 비효율적이기 때문이다. 넷플릭스나 페이스북 같은 큰 회사도 모든 것을 직접 구축하지는 않는다. 기존 클라우드를 이용할 때 서비스는 개략적으로 아래와 같이 구성되며, 이 구성을 바탕으로 비디오 업로드 절차와 비디오 스트리밍 절차 를 설계한다. ※ 단말(Client) : 컴퓨터, 모바일, 스마트 TV 등 YouTube를 시청할 수 있는 기기 ※ CDN : 비디오를 저장하고, 사용자가 재생 버튼을 누르면 CDN에서 비디오를 스트리밍하여 사용자 단말에서 이를 확인할 수 있게 함. ※ API 서버 : 비디오 스트리밍을 제외한 요청을 처리. (ex. 피드 추천, 비디오 업로드 URL 생성, 메타데이터 DB·캐시 갱신, 사용자 가입 등) 비디오 업로드 절차 해당 구조에서 비디오 업로드와 비디오 메타데이터 갱신 과정이 병렬적으로 수행된다. 비디오 업로드 원본 업로드 → 트랜스코딩 → 저장소 업로드 + 완료 이벤트 큐 → CDN 업로드 + DB/캐시 갱신 → 스트리밍 준비 완료 원본 업로드 단말이 비디오를 원본 저장소 에 업로드한다. 트랜스코딩 시작 트랜스코딩 서버가 원본 저장소에서 비디오를 가져와 여러 화질·형식으로 변환한다. 트랜스코딩 완료 후 병렬 처리 3a. 비디오 저장 : 트랜스코딩된 비디오를 트랜스코딩 비디오 저장소 에 업로드한다. 3b. 완료 이벤트 전달 : 트랜스코딩 완료 이벤트를 완료 큐 에 넣는다. 각 작업 진행 3a-1. CDN 업로드 : 트랜스코딩된 비디오를 CDN에 업로드 한다. 3b-1. 완료 이벤트 처리 : 완료 핸들러가 큐에서 이벤트를 꺼낸다. 3b-1-a/b. 메타데이터 갱신 : 완료 핸들러가 메타데이터 DB와 캐시를 갱신 한다. 스트리밍 준비 완료 알림 API 서버가 단말에 비디오 업로드 및 처리가 완료되어 스트리밍할 수 있음 을 알린다. ※ 트랜스코딩 : 압축된 디지털 파일(주로 오디오나 비디오)을 다른 형식이나 사양으로 변환하는 프로세스. 메타데이터 갱신 비디오 업로드 + 메타데이터 갱신 요청 → API 서버 → 메타데이터 캐시·DB 업데이트 메타데이터 갱신 요청 원본 저장소에 비디오를 업로드하는 동안, 단말이 API 서버에 메타데이터 갱신 요청 을 보낸다. 메타데이터 전달 요청에는 파일 이름, 파일 크기, 파일 포맷 등의 정보가 포함된다. 메타데이터 갱신 API 서버가 전달받은 정보를 이용해 메타데이터 캐시와 데이터베이스를 업데이트 한다. 비디오 스트리밍 절차 비디오는 다운로드 없이 바로 cdn에서 스트리밍된다. 이때, 비디오 스트리밍을 위해 스트리밍 프로토콜이 사용된다는 것을 알아두자. 스트리밍 프로토콜은 비디오 스트리밍을 위해 데이터를 전송할 때 쓰이는 표준화된 통신방법으로, 널리 쓰이는 것으로는 아래와 같은 것들이 있으니 훑어보자. • MPEG-DASH. MPEG • 애플(Apple) HLS. • 마이크로소프트 스무드 스트리밍 (Microsoft Smooth Streaming). • 어도비 HTTP 동적 스트리밍 (Adobe HTTP Dynamic Streaming, HDS) ※ 스트리밍 프로토콜은 각각 지원하는 비디오 인코딩이 다르고 플레이어도 다르다는 것이다. 따라서 비 디 오 스트리밍 서비스를 설계할 때는 서비스의 용례에 맞는 프로토콜을 잘 골라야 한다. 상세설계 비디오 업로드와 비디오 스트리밍을 최적화 하는 방법과, 발생할 수 있는 오류 처리에 대해서 알아본다. 비디오 트랜스코딩 비디오 트랜스코딩은 해상도, 코덱, 비트레이트(bitrate) 등의 매개변수를 조정해 비디오 파일을 다른 형식으로 변환하는 프로세스이다. 비디오를 녹화하면 단말(보통 전화나 카메라)은 해당 비디오를 특정 포맷으로 저장한다. 이 비디오가 저장 공간을 덜 차지하고, 다른 단말과 호환되며, 사용자의 대역폭에 맞는 화질로, 끊김 없이 재생되려면 변환이 필요하다. 트랜스코딩이 변환하는 대상인 비디오 포맷은 크게 두 부분으로 이루어진다. 컨테이너(container): 비디오, 오디오, 메타데이터를 담는 바구니 같은 것이다. 컨테이너 포맷은 .avi, .mov, .mp4 같은 파일 확장자를 보면 알 수 있다. 코덱(codec): 화질을 최대한 유지하면서 파일 크기를 줄이기 위해 고안된 압축 및 압축 해제 알고리즘이다. 가장 많이 쓰이는 비디오 코덱으로는 H.264, VP9, HEVC가 있다. 유향 비순환 그래프(DAG) 모델 비디오를 트랜스코딩하는 것은 컴퓨팅 자원을 많이 소모할 뿐 아니라 시간도 많이 드는 작업이다. 게다가 콘텐츠 창작자는 각자 자기만의 비디오 프로세싱 요구사항을 갖고 있다. 이런 다양한 요구를 지원하려면(=쟤들 말 들어주려면) 처리 과정을 적절히 추상화해서 작업을 직접 정의할 수 있게 해야 한다. 이때 쓸 수 있는 것이 DAG 모델이다. 원본 비디오는 비디오, 오디오, 메타데이터로 나뉘고, 정의된 작업들은 DAG 모델에 따라 단계별로 배열되어 순차적으로 또는 병렬적으로 실행된다. 비디오 트랜스코딩 아키텍처 본 설계안에서는 클라우드 서비스를 활용한 비디오 트랜스코딩 아키텍처를 다음과 같이 정의하였다. 전처리기 비디오 분할 : 비디오를 몇 초 단위의 GOP 로 나눈다. 구형 단말은 전처리기가 분할을 대신한다. DAG 생성 : 설정 파일을 바탕으로 비디오 처리 작업 순서(DAG) 를 생성한다. 데이터 캐시 : 분할된 비디오와 메타데이터를 임시 저장해 인코딩 실패 시 재개 할 수 있도록 한다. DAG 스케줄러 DAG 그래프를 몇 개 단계(stage)로 분할한 다음에 그 각각을 자원 관리자의 작업 큐(task queue)에 집어넣는다 자원 관리자 작업을 적절한 서버에 배분하고 실행 상태를 관리한다. 작업 큐 : 실행할 작업을 우선순위대로 보관 작업 서버 큐 : 사용 가능한 작업 서버 정보를 보관 실행 큐 : 현재 실행 중인 작업과 서버 정보를 보관 작업 스케줄러 : 작업과 서버를 매칭하고 실행을 지시 동작 과정: 우선순위 높은 작업 선택 → 적합한 서버 선택 → 작업 실행 지시 → 실행 큐에 등록 → 작업 완료 후 제거 작업 서버 작업 서버는 DAG에 정의된 작업을 수행한다. 이때, 작업종류에 따라 서버를 나누어 관리한다. 임시 저장소 어떤 시스템을 선택할 것이냐는 저장할 데이터의 유형, 크기, 이용 빈도, 데이터 유효기간 등에 따라 달라진다. 예시: 메타데이터 : 자주 참조하고 크기가 작으므로 메모리에 캐시 한다. 비디오·오디오 데이터 : 크기가 크므로 BLOB 저장소 에 보관한다. 임시 저장소 : 비디오 프로세싱이 완료되면 삭제한다 . 인코딩된 비디오 끝. 파이프라인 최종 결과물 생산완료. 시스템 최적화 속도, 안전성, 비용을 최적화하는 방법에 대해서 다룬다. 속도 최적화: 비디오 병렬 업로드 비디오 전체를 한 번에 업로드하는 것은 비효율적이다. 하나의 비디오를 GOP 단위로 분할해 병렬로 업로드하면 속도가 빨라지고, 일부가 실패해도 실패한 조각만 다시 올리면 되므로 빠르게 재개할 수 있다. 속도 최적화: 업로드 센터를 사용자 근거리에 지정 데이터가 이동해야 하는 네트워크 거리와 경로가 짧아지면 업로드 시간도 줄어든다. 업로드 센터를 여러 곳에 두어, 사용자가 거주지 근처의 업로드 센터를 이용하도록 하자. 속도 최적화: 모든 절차를 병렬화 메시지큐를 도입한다. 메시지 큐 도입 전: 각 모듈이 앞 단계의 작업이 끝나기를 기다려야 해서 속도가 느리다. 메시지 큐 도입 후: 모듈은 결과를 큐에 넣고 바로 다음 작업으로 넘어가고, 뒤 단계는 큐에 쌓인 작업을 여러 대가 병렬로 처리한다. 기다리는 시간이 없어지고 동시에 처리하는 양이 늘어나 속도가 빨라진다. 안전성 최적화: 미리 사인된 업로드 URL API 서버가 사용자의 업로드 권한을 확인한 뒤 미리 사인된 URL을 발급한다. 이 URL은 정해진 위치에만, 제한된 시간 동안만 쓸 수 있어서, 허가받은 사용자만 올바른 위치에 비디오를 업로드할 수 있다. 상세 절차: URL 요청: 클라이언트가 API 서버에 업로드 URL을 요청한다. URL 발급: API 서버가 업로드 권한이 포함된 미리 사인된 URL을 반환한다. 직접 업로드: 클라이언트가 해당 URL을 이용해 저장소에 비디오를 직접 업로드한다. 안전성 최적화: 비디오 보호 비디오 저작권 보호를 위해 움직여보자. 방식 핵심 내용 DRM 디지털 콘텐츠의 접근·복제 권한을 관리 AES 암호화 비디오를 암호화하고, 허가된 사용자만 재생 시 복호화 워터마크 영상에 회사 로고·소유자 정보 등을 표시 비용 최적화 CDN은 비싸다. 데이터 크기가 크면 클수록 더하다. 이 비용은 어떻게 낮출 수 있을까? 인기 영상만 CDN 사용 → 인기 없는 영상의 CDN 저장·전송 비용 을 줄인다. 인기 없는 영상은 필요할 때 인코딩 → 미리 인코딩하고 저장하는 인코딩·저장 비용 을 줄인다. 지역별 인기 영상만 해당 지역에 저장 → 다른 지역으로 영상을 복제하는 저장·전송 비용 을 줄인다. ISP와 제휴 → 사용자와 가까운 ISP를 통해 전송해 네트워크 전송 비용 을 줄인다. 사람들은 소수의 인기 영상을 본다. 인기 영상만 CDN으로 제공하고 나머지는 자체 서버에서 제공해 비용을 줄여보자. 오류 처리 시스템 오류에는 두 가지 종류가 있다. 회복 가능 오류(recoverable error) 재시도, 우회, 교체로 해결한다. 계속 실패하면 클라이언트에 오류 코드를 반환한다. 재시도로 해결 업로드 오류: 몇 회 재시도한다. 트랜스코딩 오류: 재시도한다. 전처리 오류: DAG 그래프를 재생성한다. DAG 스케줄러 오류: 작업을 다시 스케줄링한다. 작업 서버 장애: 다른 서버에서 해당 작업을 재시도한다. 다른 경로로 우회 비디오 분할 오류: 클라이언트가 GOP 단위로 분할하지 못하면 전체 비디오를 서버로 보내 서버가 분할한다. API 서버 장애: 무상태 서버이므로 신규 요청을 다른 API 서버로 보낸다. 사본과 교체로 해결 자원 관리자 큐 장애: 사본(replica)을 이용한다. 메타데이터 캐시 서버 장애: 다중화된 다른 노드에서 데이터를 가져오고, 장애 서버는 새것으로 교체한다. 메타데이터 데이터베이스 서버 장애 주 서버 장애: 부 서버 중 하나를 주 서버로 승격한다. 부 서버 장애: 다른 부 서버로 읽기를 처리하고, 장애 서버는 새것으로 교체한다. 회복 불가능 오류(non-recoverable error) 다시 시도해도 결과가 같은 오류. 해당 비디오의 작업을 중단하고 클라이언트에 오류 코드를 반환한다. 비디오 포맷이 잘못된 경우 이 주의 면접문제 14장은 260페이지를 기준으로 두 파트로 나뉘는데, 이 장의 꽃은 병렬화라고 생각해, 파트별로 병렬화와 관련된 문제를 내려고 한다. FASD92 트랜스코딩 절차에 DAG 모델을 도입하면 어떤 점이 좋은가? msms804 비디오 업로드부터 CDN 배포까지의 과정에서 속도를 최적화하는 방법으로는 업로드 센터를 사용자 가까이 두는 것이 있다. 이 외의 방법을 1가지 이상 서술하시오.
velog
⚠️ Powerlevel10k/Powerlevel9k 사용자라면 반드시 읽어보세요! Powerlevel10k나 Powerlevel9k 테마를 사용하고 있다면, Cursor AI Assistant와 터미널 작업 시 명령어 완료 감지가 제대로 되지 않는 문제를 겪고 있을 가능성이 높습니다. Cursor 공식 문서에서 이 문제를 명시적으로 언급하고 있으며, CURSOR_AGENT 환경변수를 사용하는 것이 유일한 공식 해결책 입니다. 🐛 현상 및 원인 - Cursor 공식 문서 확인 Cursor – Terminal: Run terminal commands automatically as part of agent operations Cursor 공식 문서에서 이 문제의 원인을 명확히 설명하고 있습니다: "Some shell themes (for example, Powerlevel9k/Powerlevel10k) can interfere with the inline terminal output. If your command output looks truncated or misformatted, disable the theme or switch to a simpler prompt when Agent runs." 문제 현상: Cursor AI → run_terminal_cmd → 쉘 프로세스 → 명령어 실행 ↑ ↓ 수동 Skip ←←←← 여기서 멈춤 ←←←← 출력 결과 + 새 프롬프트 실제로 일어나는 상황: ✅ 명령어가 성공적으로 실행됨 ✅ 출력 결과가 표시됨 (하지만 잘리거나 형식이 깨질 수 있음) ✅ 새로운 쉘 프롬프트가 나타남 ❌ Cursor가 "터미널 명령어 실행 중..."에서 무한 대기 🖱️ 사용자가 매번 수동으로 "Skip" 버튼을 클릭해야 함 핵심 원인: Powerlevel9k/Powerlevel10k가 인라인 터미널 출력을 방해 복잡한 프롬프트 렌더링이 Cursor의 명령어 완료 감지 메커니즘과 충돌 터미널 출력이 잘리거나 형식이 깨져서 Agent가 완료 신호를 파싱하지 못함 ✅ 해결책 - Cursor 공식 권장 방법 Cursor에서 제공하는 유일한 공식 해결책 : "Disable heavy prompts for Agent sessions" "Use the CURSOR_AGENT environment variable in your shell config to detect when the Agent is running and skip initializing fancy prompts/themes." Cursor 측에서 제공한 공식 해결책은 CURSOR_AGENT 환경변수를 사용하는 것이 유일합니다. Powerlevel10k 사용자를 위한 설정 .zshrc 파일에 다음 코드를 추가하세요: # ~/.zshrc — disable Powerlevel10k when Cursor Agent runs if [[ -n "$CURSOR_AGENT" ]]; then # Skip theme initialization for better compatibility else [[ -r ~/.p10k.zsh ]] && source ~/.p10k.zsh fi # ~/.bashrc — fall back to a simple prompt in Agent sessions if [[ -n "$CURSOR_AGENT" ]]; then PS1='\u@\h \W \$ ' fi 🛠️ 내가 사용중인 해결책 - Oh-My-Zsh + Powerlevel10k 조합 문제 상황: 공식 해결책을 그대로 적용했을 때, oh-my-zsh와 Powerlevel10k를 함께 사용하는 환경에서는 일반 터미널에서도 테마가 제대로 로드되지 않는 문제가 발생했습니다. 모든 터미널이 기본 형태가 되어버리는 현상이었죠. 해결 방법: oh-my-zsh 초기화 라인을 추가해주니 완벽하게 작동했습니다. # Oh-My-Zsh + Powerlevel10k 사용자를 위한 개선된 설정 if [[ -n "$CURSOR_AGENT" ]]; then # Agent 세션: 테마 초기화 건너뛰기 (호환성 향상) else # 일반 세션: Oh-My-Zsh와 Powerlevel10k 정상 로드 source $ZSH/oh-my-zsh.sh [[ -r ~/.p10k.zsh ]] && source ~/.p10k.zsh fi 핵심 포인트: source $ZSH/oh-my-zsh.sh 라인이 추가되어 oh-my-zsh가 정상적으로 초기화됨 Agent 터미널에서는 여전히 모든 복잡한 테마가 비활성화됨 일반 터미널에서는 oh-my-zsh와 Powerlevel10k가 완벽하게 작동함 Oh-My-Zsh + Powerlevel10k 조합 사용자라면 이 방법을 권장합니다! 마무리 Cursor에서 제공하는 공식 해결책인 CURSOR_AGENT 환경변수를 활용하면 이 문제를 완벽하게 해결할 수 있습니다.** 이것이 Cursor에서 제공하는 유일하고 공식적인 해결책**입니다. (2025.09 기준) 참조 https://docs.cursor.com/en/agent/terminal https://github.com/cursor/cursor/issues/3165 https://github.com/cursor/cursor/issues/3215 원문: Sanghun Kang의 블로그
Балл: 54.38
velog
큐를 처음 배우면 먼저 들어온 데이터가 먼저 나오는 FIFO(First In, First Out) 자료구조라고 설명한다. const queue: string[] = []; queue.push("video-101"); queue.push("video-102"); const nextVideo = queue.shift(); // "video-101" video-101 이 먼저 들어왔으므로 먼저 꺼내진다. 줄을 먼저 선 사람이 먼저 서비스를 받는 모습으로 이해하면 큐의 기본 동작을 쉽게 기억할 수 있다. 입문 단계에서는 유용한 설명이지만, 실제 서비스의 처리 순서는 push() 와 shift() 만으로 결정되지 않는다. 크리스가 동영상 두 개를 차례로 업로드하더라도 첫 번째 영상의 변환 작업이 더 오래 걸리면 두 번째 영상이 먼저 완료될 수 있다. 작업이 실패해 재시도되거나 여러 서버가 동시에 처리하면 요청 순서와 시작 순서, 완료 순서가 서로 달라진다. 사용자는 업로드 요청이 끝날 때까지 영상 변환을 기다려야 할까? 작업 서버가 감당할 수 있는 양보다 요청이 빠르게 들어오면 어떻게 해야 할까? 실패한 작업은 언제 다시 처리하며, 같은 메시지가 두 번 전달되면 결과도 두 번 만들어야 할까? 실제 서비스에서 큐는 먼저 들어온 작업을 먼저 꺼내는 저장 공간이 아니라, 작업이 들어오는 속도와 처리되는 속도를 분리하고 어떤 작업을 언제 실행할지 통제하는 구조다. 오래 걸리는 작업을 요청 안에서 끝내야 할까? 크리스가 2GB짜리 영상을 업로드했다고 해보자. 서버는 원본 파일을 저장한 뒤 여러 해상도로 변환하고 썸네일을 생성해야 한다. 처음에는 하나의 API 요청에서 모든 작업을 처리할 수 있다. async function uploadVideo(input: UploadInput) { const video = await saveOriginalVideo(input.file); await transcodeVideo(video.id); await createThumbnail(video.id); return { videoId: video.id, status: "ready" }; } 코드는 단순하지만 변환에 10분이 걸리면 API 응답도 10분 동안 끝나지 않는다. 연결이 끊기거나 서버가 재시작되면 어디까지 처리했는지 확인하기도 어렵다. 동시에 여러 사용자가 영상을 올리면 업로드 요청이 변환 작업에 서버 자원을 빼앗길 수 있다. 영상 업로드와 영상 변환은 같은 시점에 시작할 수 있지만 생명주기가 다르다. 업로드 요청은 원본 파일이 안전하게 저장되고 작업이 등록되면 끝낼 수 있다. 영상 변환은 별도의 작업 서버가 이후에 처리해도 된다. async function uploadVideo( userId: string, input: UploadInput ) { const validatedFile = validateVideoFile(input.file); const video = await saveOriginalVideo(userId, validatedFile); const job = await videoJobRepository.create({ videoId: video.id, type: "transcode", status: "queued", attemptCount: 0 }); await videoQueue.enqueue({ jobId: job.id }); return { videoId: video.id, status: "processing" }; } 서버는 파일 형식과 크기, 사용자의 업로드 권한을 확인한 뒤 원본 영상을 저장한다. 그다음 변환 작업을 등록하고 큐에는 작업 ID를 넣는다. 클라이언트는 변환이 끝날 때까지 연결을 유지하는 대신 processing 상태를 받고 나중에 결과를 조회한다. 현실의 영상 처리 과정은 다음과 같이 코드로 변환된다. 입력 사용자 ID, 원본 영상, 변환 설정 상태 영상 ID, 원본 파일 위치, 작업 상태, 시도 횟수 출력 변환된 영상과 썸네일 또는 실패 상태 큐를 도입하면서 요청 처리와 작업 처리가 분리된다. 이제 API 서버는 작업을 안전하게 등록할 책임을 지고, 작업 서버는 큐에서 작업을 가져와 실행하고 결과를 기록할 책임을 진다. 먼저 꺼낸 작업이 먼저 완료된다는 보장은 없다 하나의 작업 서버가 모든 영상을 차례로 처리한다면 큐에 들어온 순서와 작업 시작 순서가 대부분 일치한다. 그러나 처리량을 높이기 위해 작업 서버를 세 개로 늘리면 결과가 달라진다. async function worker() { const message = await videoQueue.dequeue(); const job = await videoJobRepository.findById(message.jobId); await transcodeVideo(job.videoId); } 첫 번째 서버가 30분짜리 영상을 처리하는 동안 두 번째 서버는 1분짜리 영상을 처리할 수 있다. 먼저 등록된 영상이 늦게 끝나는 것은 오류가 아니라 동시 처리의 자연스러운 결과다. video-101: 30분 영상 ───────────────── 완료 video-102: 1분 영상 ── 완료 따라서 큐에서 말하는 순서는 구분해서 생각해야 한다. 큐에 등록된 순서 작업 서버가 가져간 순서 작업을 시작한 순서 결과가 완료된 순서 일반적인 영상 변환에서는 완료 순서가 바뀌어도 문제가 없다. 각 영상이 독립적이기 때문이다. 하지만 한 영상에 대해 원본 분석 → 변환 → 썸네일 생성 → 공개 순서가 반드시 지켜져야 한다면 각 작업의 선행 조건을 확인해야 한다. async function publishVideo(videoId: string) { const video = await videoRepository.findById(videoId); if (video.transcodeStatus !== "completed") { throw new Error("Video has not been transcoded"); } if (video.thumbnailStatus !== "completed") { throw new Error("Thumbnail has not been created"); } await videoRepository.updateStatus(videoId, "published"); } 큐에 먼저 넣었다는 사실만으로 작업 의존성이 보장되지는 않는다. 공개 작업은 필요한 결과가 준비되었는지 원본 상태를 확인해야 한다. 순서가 비즈니스 규칙이라면 큐의 FIFO 동작에만 기대지 말고 상태와 선행 조건으로 표현해야 한다. 큐의 메시지를 원본 데이터로 사용해도 될까? 변환에 필요한 모든 정보를 메시지에 넣을 수도 있다. await videoQueue.enqueue({ videoId: "video-101", ownerId: "user-20", sourceUrl: "https://storage.example.com/original.mp4", resolution: "1080p", status: "queued" }); 이 메시지가 오래 대기하는 동안 사용자가 영상을 삭제하거나 관리자가 공개를 제한할 수 있다. 저장 위치가 변경되거나 변환 설정이 수정될 수도 있다. 작업 서버가 메시지에 들어 있는 오래된 상태를 그대로 사용하면 삭제된 영상을 다시 처리하거나 현재 정책과 다른 결과를 만들 수 있다. 큐에는 가능한 한 작업을 식별할 수 있는 최소한의 정보만 넣고, 실행 직전에 신뢰할 수 있는 저장소에서 현재 상태를 조회하는 편이 안전하다. await videoQueue.enqueue({ jobId: job.id }); async function processVideoJob(jobId: string) { const job = await videoJobRepository.findById(jobId); if (!job || job.status === "completed") { return; } const video = await videoRepository.findById(job.videoId); if (!video || video.status === "deleted") { await videoJobRepository.markCancelled(job.id); return; } await transcodeVideo(video); await videoJobRepository.markCompleted(job.id); } 데이터베이스의 작업 레코드가 작업 상태의 Source of Truth가 되고, 큐 메시지는 해당 작업을 실행하라는 신호가 된다. 큐에 메시지가 남아 있더라도 데이터베이스에서 작업이 취소된 상태라면 실행하지 않는다. 외부에서 작업 ID를 받았다고 해서 바로 처리해서도 안 된다. 작업의 존재 여부, 영상의 소유자와 상태, 허용된 변환 설정을 확인해야 한다. 큐가 내부 시스템에 있더라도 메시지는 지연되거나 중복될 수 있으므로 검증 전까지 현재 상태라고 가정할 수 없다. 같은 작업이 두 번 전달되면 어떻게 해야 할까? 많은 작업 대기열은 메시지가 절대로 중복되지 않는다고 보장하지 않는다. 작업 서버가 변환을 완료한 직후 결과 확인 메시지를 보내기 전에 종료되면, 큐는 처리되지 않은 것으로 판단해 같은 작업을 다시 전달할 수 있다. async function processVideoJob(jobId: string) { const job = await videoJobRepository.findById(jobId); await transcodeVideo(job.videoId); await videoJobRepository.markCompleted(job.id); } 이 구현에서는 같은 메시지가 두 번 전달될 때 변환 파일이 중복 생성될 수 있다. 사용자에게 완료 알림을 보내는 작업이라면 같은 알림이 여러 번 전송될 수도 있다. 작업 서버는 이미 완료된 작업인지 확인하고, 실행 중 상태로 안전하게 변경한 서버만 실제 처리를 시작해야 한다. async function processVideoJob(jobId: string) { const claimed = await videoJobRepository.claimIfQueued(jobId); if (!claimed) { return; } try { await transcodeVideo(claimed.videoId); await videoJobRepository.markCompleted(jobId); } catch (error) { await videoJobRepository.markFailed(jobId); throw error; } } claimIfQueued() 는 작업 상태가 queued 일 때만 processing 으로 바꾸고, 동시에 여러 작업 서버가 요청하더라도 하나만 성공하도록 구현해야 한다. 데이터베이스의 조건부 업데이트나 트랜잭션이 이 책임을 맡을 수 있다. 이처럼 같은 작업이 여러 번 전달되어도 최종 결과가 한 번 처리된 것과 같도록 만드는 성질을 멱등성이라고 한다. 큐를 사용하는 서비스에서는 메시지가 정확히 한 번만 전달될 것이라고 기대하기보다, 중복 전달을 견딜 수 있도록 작업을 설계하는 편이 안전하다. 실패한 작업을 무조건 다시 넣어도 될까? 영상 변환이 실패했다고 해서 모두 같은 방식으로 재시도할 수 있는 것은 아니다. 작업 서버의 일시적인 메모리 부족은 잠시 뒤 성공할 수 있지만, 손상된 영상 파일은 몇 번을 반복해도 실패할 가능성이 높다. async function handleFailure( job: VideoJob, error: Error ) { if (job.attemptCount < 3 && isTemporary(error)) { await videoJobRepository.incrementAttempt(job.id); await videoQueue.retry(job.id); return; } await videoJobRepository.markPermanentlyFailed( job.id, error.message ); } 재시도 횟수와 간격을 제한하지 않으면 실패 작업이 계속 큐로 돌아와 정상 작업의 처리를 방해할 수 있다. 실패 원인을 구분하고, 일시적인 오류만 일정 시간 후 다시 처리하며, 반복해서 실패한 작업은 별도로 격리해야 한다. 요청이 작업 서버의 처리 속도보다 빠르게 들어오는 상황도 고려해야 한다. 한 시간에 1,000개의 영상이 등록되는데 500개만 처리할 수 있다면 큐는 계속 길어진다. 큐가 있다는 이유만으로 처리 능력이 증가하지는 않는다. 대기 작업 수와 가장 오래된 작업의 대기 시간, 실패율을 관찰하고 작업 서버를 늘리거나 업로드 속도를 제한해야 한다. 큐는 부하를 없애는 장치가 아니라 부하가 한꺼번에 작업 서버로 전달되지 않도록 완충하고, 운영자가 처리량을 조절할 시간을 제공하는 장치다. 영상 처리 큐를 설계하기 전에 확인할 질문 API 요청에서 즉시 끝내야 하는 일과 나중에 처리해도 되는 일은 무엇인가? 큐에 들어간 순서, 작업 시작 순서와 완료 순서 중 어떤 순서를 보장해야 하는가? 여러 작업 서버가 동시에 처리해도 각 작업은 서로 독립적인가? 선행 작업이 필요한 경우 그 조건을 큐 순서가 아니라 상태로 검증하고 있는가? 작업 상태의 Source of Truth는 큐 메시지인가, 데이터베이스의 작업 레코드인가? 오래된 메시지를 처리하기 전에 영상의 현재 상태와 접근 권한을 다시 확인하는가? 같은 메시지가 두 번 전달돼도 결과가 중복 생성되지 않는가? 일시적인 실패와 다시 시도해도 해결되지 않는 실패를 구분하는가? 재시도 횟수와 간격, 영구 실패 후의 처리 방법이 정해져 있는가? 작업 유입 속도가 처리 속도보다 빨라질 때 대기 시간과 서버 용량을 어떻게 통제할 것인가? 큐는 작업의 속도와 실행 순서를 통제하는 경계다 큐의 기본 동작은 먼저 들어온 항목을 먼저 꺼내는 것이다. 그러나 실제 영상 서비스에서는 작업을 꺼내는 순서만으로 전체 처리 순서를 설명할 수 없다. 여러 작업 서버가 동시에 실행되고, 오래 걸리는 작업과 실패한 작업이 섞이며, 같은 메시지가 다시 전달될 수 있기 때문이다. 영상 업로드 요청과 변환 작업을 분리하면 사용자는 긴 처리를 기다리지 않아도 되고, 서버는 감당할 수 있는 속도로 작업을 가져갈 수 있다. 대신 작업 상태를 저장하고, 중복 실행을 방지하고, 실패와 재시도를 통제하며, 순서가 필요한 작업의 선행 조건을 검증해야 한다. 큐를 선택한다는 것은 단순히 FIFO 자료구조를 사용하는 일이 아니다. 요청이 들어오는 시점과 실제 작업이 실행되는 시점 사이에 경계를 만들고, 그 경계에서 처리량·순서·실패를 관리하겠다는 설계 판단이다. 다음 글에서는 힙과 우선순위 큐가 모든 작업을 정렬하지 않고도 지금 처리해야 할 가장 중요한 작업을 선택하는 방법을 살펴본다.
velog
구글 안티그래비티(agy) CLI를 써보다 마주친 오류 7가지를 직접 재현해서 원인과 조치를 정리한 글입니다. command not found 는 PATH에 ~/.local/bin 이 빠진 문제였고, 로그인 전에 헤드리스로 돌리면 authentication timed out 이 나서 대화형으로 먼저 로그인해야 했습니다. 모델을 고를 때 --effort 값을 안 주면 거부당하고, 503은 20~30초 후 재시도하면 풀렸습니다. 가장 까다로웠던 건 파일 쓰기 권한 거부인데, settings.json 에 허용 규칙을 넣어도 경로가 심볼릭 링크로 풀리는 형태(/tmp vs /private/tmp)와 안 맞으면 조용히 거부되면서 종료 코드는 0이 나왔습니다. 성공과 거부를 가르는 신호가 종료 코드가 아니라 출력 문구와 실제 파일 생성 여부라는 게 핵심입니다. 오류 7가지 한눈에 증상 | 원문 문구(일부) | 종료코드 | 조치 command not found | zsh:1: command not found: agy | 127 | PATH 추가 로그인 타임아웃 | Error: authentication timed out | 1 | 대화형 로그인 먼저 not logged into 로그 | You are not logged into... | 0 | 로그가 아니라 출력으로 판정 모델 선택 거부 | requires --effort | 1 | --effort high/low 지정 서버 503 | UNAVAILABLE (code 503) | 1 | 20~30초 후 재실행 파일 쓰기 거부 | so it was auto-denied | 0 | 허용 규칙 경로를 실경로로 한국어 응답 안 됨 | 영어 질문에 영어로 답 | - | AGENTS.md로 지시 권한 거부는 종료 코드로 못 잡는다 agy -p "현재 폴더에 hello.txt 파일을 만들고 hi 라고 써줘" # 출력: ...so it was auto-denied... # 종료 코드 0, hello.txt는 생성되지 않음 settings.json 에 /tmp/agy-try2 를 허용해도 실제로는 /private/tmp/agy-try2 로 풀려 있어 계속 거부됐고, 실경로로 바꾸고서야 파일이 만들어졌습니다. 전체 과정과 실패 사례는 원글에 정리했습니다: https://blog.wonizz.com/2026/10/03/antigravity-cli-errors/?utm_source=velog&utm_medium=community&utm_campaign=antigravity-cli-errors&utm_content=daily
Балл: 54.37Уверенность: 49%
Подробнееvelog
0. 들어가며 지난 글에서는 여러 오픈소스 Agent Framework의 실제 실행 코드를 따라가며 Agent Loop가 어떤 단위로 반복되고, 각 프레임워크가 그 실행 결과를 어떻게 표현하는지 살펴봤습니다. 이번 글에서는 여기서 한 단계 더 들어가 누가 Agent의 Control Flow를 소유하는지 살펴보겠습니다. Model이 Tool을 호출하거나 다른 Agent를 선택하면 그 결정이 그대로 다음 실행으로 이어지는지, Framework의 Runner가 별도의 상태 전이를 결정하는지, 또는 LangGraph처럼 처음부터 Graph Runtime이 실행 가능한 Node와 경로를 관리하는지 실제 코드를 따라가 보겠습니다. 특히 OpenAI Agents SDK의 Agent Loop와 LangGraph의 Graph Runtime을 중심으로 비교 하고, Google ADK와 PydanticAI처럼 두 접근이 겹쳐 있는 구조도 함께 살펴보겠습니다. 이번 글에서는 OpenAI Agents SDK만 먼저 보겠습니다. 전체 구조 요약 Model + Runtime + Runner 구조: Control Flow의 각 단계를 역할별로 분담 Control Flow 단계 주체 무엇을 결정하는가 제품에서의 의미 Action Decision Model 현재 Context를 바탕으로 Tool 호출, Handoff 요청, 응답 생성 중 다음 행동을 선택 Agent가 현재 상황에 맞는 다음 행동을 선택 Output Interpretation Runtime Model Output을 현재 실행 환경에서 어떤 작업으로 처리할지 해석 Model Output을 바로 실행하지 않고 Approval, Guardrail, 현재 실행 상태 등의 정책을 적용 Transition Resolution Runtime Tool과 Side Effect 처리 결과를 바탕으로 이번 Turn을 어떤 상태로 끝낼지 결정 승인 대기, 실행 중단, Final Output, Agent 전환 등의 상태를 제품에서 명시적으로 관리 Loop Advancement AgentRunner 확정된 Control State를 실제 다음 실행에 반영 같은 Agent로 다음 Turn을 진행하거나, Agent를 교체하거나, 실행을 중단·재개·종료 1. Control Flow가 무엇인가요? 먼저 이 글에서 말하는 Control Flow 가 무엇인지부터 정리해보겠습니다. Control Flow는 Agent 분야에서 새로 등장한 개념이 아니라 프로그래밍 언어와 컴퓨터과학에서 오래전부터 사용해 온 개념이라고 하는데요. NIST의 Dictionary for Information Systems는 ISO의 정의를 인용해 Control Flow를 다음과 같이 설명합니다. “an abstraction of all possible paths that an execution sequence may take through a program.” 프로그램의 실행 시퀀스가 따를 수 있는 모든 가능한 경로의 추상화 가장 단순한 프로그램은 위에서 아래로 순서대로 실행되지만, 실제 프로그램에는 조건문과 반복문, 함수 호출, 예외 처리 같은 구조가 들어갑니다. 예를 들어 다음 코드에서는 is_valid 값에 따라 실행할 코드가 달라집니다. if is_valid: process() else: reject() 여기서 중요한 것은 process() 와 reject() 가 무엇을 하는지가 아니라, 둘 중 무엇을 다음에 실행할지를 결정하는 구조 입니다. 이것이 Control Flow입니다. Agent에서도 같은 질문을 할 수 있습니다. Model이 응답을 생성한 뒤 Agent는 다음에 무엇을 해야 할까요? Tool 실행 같은 Model을 다시 호출 다른 Agent에게 실행을 넘기기 (Handoff) Human Approval을 기다리며 멈추기 (HITL) 충분한 결과가 만들어졌다면 실행 종료 즉 Agent의 Control Flow는 현재 실행 상태에서 다음에 어떤 작업을 실행할지 결정하고, 그 실행을 다음 단계로 연결하는 방식 입니다. 예를 들어 전형적인 Agent Loop에서는 Model이 Tool Call을 생성하면 Tool을 실행하고, 그 결과를 다시 Model에 전달합니다. 반면 Graph 기반 Runtime에서는 미리 정의된 Node와 Edge, 현재 State를 바탕으로 다음에 실행할 Node를 결정할 수 있습니다. 이 두 구조는 모두 Model과 Tool을 사용할 수 있지만, 다음 실행 경로를 결정하는 방식이 다릅니다. 그래서 이번 글에서 Control Flow를 살펴볼 때에는 크게 세 가지 축에서 나눠서 살펴보고자 합니다. Action Decision 현재 상황에서 Tool을 호출할지, 답변할지, 다른 Agent에게 넘길지 누가 선택하는가 Transition Resolution Model이나 Tool의 실행 결과를 보고 다음 상태를 누가 확정하는가 Execution Topology 애초에 어떤 실행 경로가 존재할 수 있는지를 어디에서 정의하는가 1. Agent Loop의 Control Flow — OpenAI Agents SDK 이제 실제 코드 수준에서 Control Flow를 살펴보겠습니다. 지난 글에서는 OpenAI Agents SDK의 한 Turn이 끝나면 결과가 NextStepRunAgain, NextStepHandoff, NextStepInterruption, NextStepFinalOutput 중 하나로 정리된다는 것까지 살펴봤습니다. OpenAI Agents SDK는 Control Flow를 어떻게 구현하고 있을까요? AgentRunner : Loop를 돌리는 주체 먼저 가장 바깥쪽인 src/agents/run.py 부터 보겠습니다. Runner.run() 의 설명에는 Agent를 Final Output이 만들어질 때까지 Loop로 실행한다고 되어 있습니다. """ Run a workflow starting at the given agent. The agent will run in a loop until a final output is generated. The loop runs like so: 1. The agent is invoked with the given input. 2. If there is a final output, the loop terminates. 3. If there's a handoff, we run the loop again, with the new agent. 4. Else, we run tool calls (if any), and re-run the loop. """ Source: OpenAI Agents SDK · src/agents/run.py · Runner.run() · https://raw.githubusercontent.com/openai/openai-agents-python/main/src/agents/run.py Handoff가 발생하면 새 Agent로 다시 Loop를 실행하고, 그 외 Tool Call이 있다면 Tool을 실행한 뒤 다시 Loop를 돈다는 구조입니다. 한 번의 Turn은 하나의 AI invocation과 그 과정에서 발생하는 Tool Call을 묶은 단위 입니다. 실제 실행부를 보면, AgentRunner 가 Turn을 직접 세면서 run_single_turn() 을 호출합니다. current_turn += 1 ... turn_result = await run_single_turn(...) Source: OpenAI Agents SDK · src/agents/run.py · AgentRunner.run() 간단히 도식화해볼까요? 하지만 AgentRunner 가 직접 Model을 호출하고 결과를 보고 Tool 실행 여부까지 판단하는 것은 아닙니다. Runner는 말 그대로 '실행시키는 역할'이죠. 실제 처리는 run_single_turn() 아래에서 여러 단계로 나뉩니다. run_single_turn() - 호출된 결과 처리 src/agents/run_internal/run_loop.py 의 run_single_turn() 으로 내려가 보겠습니다. 이 함수는 먼저 현재 Agent를 기준으로 System Prompt, Tool, Handoff, Output Schema 등을 준비합니다. 우선 현재 execution_agent에서 System Prompt와 Prompt 설정을 가져오고, 사용할 Handoff와 Output Schema를 결정합니다. system_prompt, prompt_config = await gather_with_cancel( execution_agent.get_system_prompt(context_wrapper), execution_agent.get_prompt(context_wrapper), ) handoffs = await get_handoffs(execution_agent, context_wrapper) output_schema = get_output_schema(execution_agent) Source: OpenAI Agents SDK · src/agents/run_internal/run_loop.py · run_single_turn() · 2026-10-02 · line 1941-1958 중간에 resolve_tool_name_collisions() 도 호출합니다. 현재 Turn에서 사용할 Tool과 Handoff가 Model 입장에서 같은 이름으로 노출될 수 있기 때문에, run_config.tool_name_collision_policy에 따라 이름 충돌을 먼저 정리하는 역할입니다. 이 단계가 끝나면 이번 Model Call에 전달할 Tool / Handoff 목록이 확정됩니다. 이후 현재까지의 입력과 생성된 Item을 Provider Input으로 만든 다음 get_new_response() 를 호출합니다. new_response = await get_new_response(...) ... return await get_single_step_result_from_response(...) Source: OpenAI Agents SDK · src/agents/run_internal/run_loop.py · run_single_turn() get_new_response() 안에서는 실제 model.get_response() 가 호출됩니다. 이 단계에서 다음 값들이 전달됩니다. system_instructions input model_settings tools output_schema handoffs Model은 이 정보를 보고 Text Response를 만들거나 Function Tool을 호출하고, Handoff에 해당하는 Tool을 호출하는 등의 선택을 할 수 있습니다. 여기까지 오면 Model이 ModelResponse 를 반환합니다. ModelResponse 는 먼저 ProcessedResponse 로 변환된다 그런데 Model 호출이 끝나면 run_single_turn() 은 ModelResponse 를 그대로 실행하지 않고, ProcessedResponse 라는 최종 결과물을 만듭니다. 다음 단계인 get_single_step_result_from_response() 에 넘기고, 여기서 가장 먼저 process_model_response() 를 호출합니다. processed_response = process_model_response( agent=item_agent, all_tools=all_tools, response=new_response, output_schema=output_schema, handoffs=handoffs, existing_items=pre_step_items, run_config=run_config, server_manages_conversation=server_manages_conversation, ... ) ... return await execute_tools_and_side_effects( ... new_response=new_response, processed_response=processed_response, ... ) Source: OpenAI Agents SDK · src/agents/run_internal/turn_resolution.py · get_single_step_result_from_response() · 2026-10-02 · line 2852-2909 이 함수가 하는 일은 Model Provider가 반환한 출력을 Agent Runtime이 이해하고 실행할 수 있는 의미 단위로 해석하는 것 이고 그 결과물이 바로 ProcessedResponse 라고 이해할 수 있습니다. Model Response + 추가 입력 실제 함수 signature부터 살펴보겠습니다. def process_model_response( *, agent: Agent[Any], all_tools: list[Tool], response: ModelResponse, output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], existing_items: Sequence[RunItem] | None = None, run_config: RunConfig | None = None, server_manages_conversation: bool = False, server_managed_input_items: Sequence[Any] | None = None, allow_apply_patch_function_fallback: bool = True, ) -> ProcessedResponse: Source: OpenAI Agents SDK · src/agents/run_internal/turn_resolution.py · process_model_response() · 2026-10-02 · line 2138-2150 response 하나만 받는 것이 아니라 현재 Agent, 사용할 수 있는 Tool 전체, Handoff 목록, Output Schema, 이전에 생성한 Item, RunConfig까지 함께 받습니다. 그래야 Model이 출력한 ModelResponse 항목이 Runtime에서 무엇을 의미하는지 결정할 수 있기 때문 입니다. 예를 들어 Model이 다음 Function Call을 반환했다고 해보겠습니다. transfer_to_support(...) 얼핏 보면 그냥 함수 호출이지만, 실제로는 일반 Function Tool이 아닐 수 있습니다. 예를 들어 현재 Agent에 transfer_to_support 라는 Handoff가 등록되어 있다면 이 호출은 Tool 실행이 아니라 Agent를 교체하기 위한 Handoff 요청 으로 해석해야 합니다. 그래서 process_model_response() 는 먼저 현재 실행 환경을 바탕으로 lookup map을 만듭니다 (이외에도 Shell, Apply Patch, Code Interpreter, Programmatic Tool, Hosted MCP 등을 각각 찾아둡니다). handoff_map = {handoff.tool_name: handoff for handoff in handoffs} function_map = build_function_tool_lookup_map( [tool for tool in all_tools if isinstance(tool, FunctionTool)] ) custom_tool_map = { tool.name: tool for tool in all_tools if isinstance(tool, CustomTool) } computer_tool = next( (tool for tool in all_tools if isinstance(tool, ComputerTool)), None, ) Source: OpenAI Agents SDK · src/agents/run_internal/turn_resolution.py · process_model_response() · 2026-10-02 · line 2151-2184 그리고 response.output 을 하나씩 순회하면서 각 Output Item이 실제로 무엇인지 판별합니다. for output in response.output: output_type = get_mapping_or_attr(output, "type") ... Source: OpenAI Agents SDK · src/agents/run_internal/turn_resolution.py · process_model_response() · 2026-10-02 · line 2202-2210 특히 Function Call 처리 부분을 보면 이 함수의 역할이 명확하게 드러납니다. if is_handoff_tool_call(output, handoff_map): ... items.append(HandoffCallItem(raw_item=output, agent=agent)) handoff = ToolRunHandoff( tool_call=output, handoff=handoff_map[output.name], ) run_handoffs.append(handoff) else: lookup_key = get_function_tool_lookup_key_for_call(output) func_tool = function_map.get(lookup_key) if lookup_key is not None else None ... items.append( ToolCallItem( raw_item=output, agent=agent, ... ) ) functions.append( ToolRunFunction( tool_call=output, function_tool=func_tool, ) ) Source: OpenAI Agents SDK · src/agents/run_internal/turn_resolution.py · process_model_response() · 2026-10-02 · line 2755-2836 같은 ResponseFunctionToolCall 이라도 현재 Runtime에 어떤 Handoff와 Tool이 등록되어 있는지를 확인한 뒤 서로 다른 의미로 바꾸고 있습니다. Handoff 이름과 일치하면 ToolRunHandoff Function Tool과 일치하면 ToolRunFunction Tool을 찾지 못하면 설정에 따라 Error 처리 또는 ToolRunFunctionNotFound Apply Patch와 같은 특수 Tool이면 별도의 실행 타입 즉 ModelResponse 는 Model이 무엇을 출력했는지 를 나타내는 객체이고, ProcessedResponse 는 그 출력이 현재 Agent Runtime에서 무엇을 의미하는지 해석한 결과 라고 볼 수 있습니다. ProcessedResponse 정의 더 뜯어보기 ProcessedResponse 의 class 정의를 조금 더 보겠습니다. @dataclass class ProcessedResponse: new_items: list[RunItem] handoffs: list[ToolRunHandoff] functions: list[ToolRunFunction] computer_actions: list[ToolRunComputerAction] local_shell_calls: list[ToolRunLocalShellCall] shell_calls: list[ToolRunShellCall] apply_patch_calls: list[ToolRunApplyPatchCall] tools_used: list[str] mcp_approval_requests: list[ToolRunMCPApprovalRequest] interruptions: list[ToolApprovalItem] function_tools_not_found: list[ToolRunFunctionNotFound] = ... custom_tool_calls: list[ToolRunCustom] = ... Source: OpenAI Agents SDK · src/agents/run_internal/run_steps.py · ProcessedResponse · 2026-10-02 · line 110-124 보면 크게 중요한 두 부류가 있는데요. 첫 번째는 new_items 입니다. MessageOutp
velog
Mutation Arcade의 다음 버전을 생각하면서 가장 먼저 바뀐 것은 게임의 구조였습니다. Web Beta에서는 여러 개의 Arcade를 메뉴에서 선택하면 됐습니다. 하지만 다음 버전에서는 플레이어 캐릭터가 직접 공간을 돌아다니고, 무언가를 발견하고, 상호작용한 뒤 Arcade를 플레이하는 구조를 만들고 싶었습니다. 그러자 바로 문제가 생겼습니다. 이 미니게임들은 왜 같은 공간에 존재하는가? 단순히 여러 Arcade를 하나의 맵 안에 배치하는 것만으로는 하나의 게임처럼 느껴지기 어려웠습니다. 그래서 저는 미니게임을 먼저 이어붙이는 대신, 그 미니게임들이 존재할 수 있는 하나의 세계부터 만들기로 했습니다. 단순한 연결로는 부족했습니다 처음에는 이렇게 만들 수도 있었습니다. AREA A → ARCADE 1 → AREA B → ARCADE 2 → AREA C 플레이어가 이동하면서 차례대로 여러 미니게임을 플레이하는 방식입니다. 기능적으로는 충분히 가능합니다. 하지만 이 구조에는 한 가지 문제가 있었습니다. 왜 플레이어가 이 게임들을 해야 하는지 설명되지 않습니다. 단순히 다음 구역으로 가기 위해 미니게임 하나를 플레이하고, 또 다음 구역에서 다른 미니게임을 플레이하는 방식이라면, 결국 Web Beta의 메뉴를 공간 안으로 옮겨놓은 것과 크게 다르지 않을 수도 있었습니다. 메뉴 버튼 대신 맵 안의 장치를 누르는 것뿐이라면 굳이 캐릭터와 공간을 만든 의미가 약해집니다. 공간을 만들면 이유도 필요해집니다 플레이어가 직접 움직이는 순간부터 게임은 계속 질문을 만들었습니다. 왜 이곳에 있는가. 이곳은 어떤 장소인가. 왜 Arcade 장치가 존재하는가. 왜 정상적인 Arcade가 아니라 Mutation이 발생하는가. 다음 공간으로 이동하는 이유는 무엇인가. 이전에는 각 Arcade 내부의 규칙만 만들면 됐습니다. 하지만 이제는 Arcade 밖의 상황까지 설명해야 했습니다. WEB BETA ARCADE ARCADE ARCADE 각 게임이 독립적 ↓ NEXT VERSION WORLD ├─ PLACE ├─ PLAYER ├─ EVENT ├─ ARCADE └─ MUTATION 모든 요소가 연결됨 이때부터 Mutation Arcade는 단순한 미니게임 컬렉션과는 다른 문제를 풀기 시작했습니다. 먼저 이야기를 만들어보기로 했습니다 그렇다고 처음부터 게임 안에 긴 스토리를 넣으려 했던 것은 아닙니다. 먼저 필요했던 것은 게임 전체를 설명할 수 있는 기준이었습니다. 플레이어가 누구인지, 어떤 상황에서 게임이 시작되는지, 어떤 공간을 지나게 되는지, 그 안에서 무엇이 벌어졌는지, Mutation이 이 세계와 어떤 관계를 가지는지. 이런 것들이 정해져야 맵도 만들 수 있고, 각 Stage의 역할도 정할 수 있다고 생각했습니다. 그래서 바로 Unity에서 맵을 만들기 시작하는 대신, 먼저 전체 이야기를 정리하기 시작했습니다. 이번에는 이야기를 꽤 길게 만들었습니다 여기서 선택한 방식은 단순한 게임 시놉시스를 작성하는 것보다 조금 더 길었습니다. 저는 Mutation Arcade의 전체 이야기를 하나의 소설처럼 먼저 만들어보기로 했습니다. 게임을 만들기 전에 내부 원작에 가까운 이야기를 하나 만들어두는 방식입니다. 물론 이 이야기를 실제로 출간하려는 것은 아니었습니다. 사용자에게 그대로 보여줄 텍스트를 먼저 쓰는 것도 아니었습니다. 목적은 따로 있었습니다. 게임을 만들 때 계속 참고할 수 있는 하나의 원본 이야기를 확보하는 것 이었습니다. 왜 굳이 소설 형태였을까 간단한 설정 문서만으로도 세계관은 만들 수 있습니다. 하지만 공간과 사건을 조금 더 구체적으로 생각하려면 장면 단위로 상상해볼 필요가 있었습니다. 누가 어디에서 무엇을 보고, 어떤 사건이 먼저 발생하고, 그다음에 어디로 이동하며, 그 공간에는 어떤 흔적이 남아 있는지. 이런 내용을 이야기 형태로 풀어놓으면 단순한 설정 목록보다 게임 장면을 떠올리기가 쉬웠습니다. SETTING + CHARACTER + EVENT + PLACE + SEQUENCE ↓ INTERNAL STORY 그리고 이 이야기는 나중에 게임으로 다시 분해할 수 있습니다. 이야기를 만든 뒤 다시 게임 구조로 바꾸면 됩니다 소설 형태로 만든다고 해서 게임을 텍스트 중심으로 만들려는 것은 아니었습니다. 오히려 반대였습니다. 먼저 이야기 전체를 만들어놓고, 그 안에서 게임에 필요한 부분만 다시 추출하려고 했습니다. 예를 들면, 어떤 사건이 발생하는가 어느 공간에서 발생하는가 플레이어는 무엇을 발견하는가 어떤 상호작용이 필요한가 어디에서 Arcade가 등장하는가 어떤 상태가 다음 구간으로 이어지는가 같은 요소들입니다. 즉, STORY → GAME STRUCTURE 로 변환하는 방식입니다. 이렇게 하면 각 Stage가 단순히 다음 미니게임으로 이동하는 통로가 아니라, 전체 이야기 안에서 각자의 역할을 가질 수 있다고 생각했습니다. Arcade에도 이유를 만들고 싶었습니다 이전 Web Beta에서 Arcade는 그 자체로 목적이었습니다. 게임을 선택하고, 게임을 플레이하면 됩니다. 하지만 새로운 구조에서는 Arcade도 세계 안에 존재해야 했습니다. 그렇다면 왜 이 Arcade를 만나게 되는가? 왜 지금 플레이해야 하는가? 플레이 결과가 무엇과 연결되는가? 같은 질문이 필요합니다. Arcade를 단순한 콘텐츠 하나가 아니라 이야기 속 사건이나 상황과 연결하면, 플레이어 입장에서도 미니게임 하나를 선택해서 플레이하는 것이 아니라 게임 세계 안에서 어떤 상황에 마주쳤다는 느낌 을 만들 수 있을 것이라고 생각했습니다. Mutation도 다시 설명해야 했습니다 세계관을 만들기 시작하자 Mutation 역시 다시 보게 됐습니다. Web Beta에서 Mutation은 게임 플레이를 바꾸는 시스템이었습니다. Blocks의 형태가 변하고, Breaker의 반사가 달라지고, Snake의 조작이 흔들리는 식입니다. 그런데 하나의 세계를 만들려면 여기서 질문이 하나 더 생깁니다. 왜 Mutation이 발생하는가? 게임 시스템 관점에서는 그냥 재미를 위한 규칙 변화라고 말할 수 있습니다. 하지만 게임 세계 안에서는 다른 설명이 필요합니다. 이 질문 때문에 Mutation도 단순한 게임 시스템을 넘어 세계관 안에서 다시 정의할 필요가 생겼습니다. 다만 이 부분은 이후 플레이 경험과 직접 연결되는 내용이기 때문에 여기서는 구체적으로 공개하지 않겠습니다. 먼저 세계를 만들고, 그 안에 게임을 넣었습니다 결과적으로 새로운 Mutation Arcade를 준비하는 순서는 Web Beta 때와 완전히 달라졌습니다. 처음에는: IDEA → CODE → PLAY 에 가까웠습니다. 이번에는: WORLD ↓ STORY ↓ PLACE ↓ EVENT ↓ GAME STRUCTURE ↓ IMPLEMENTATION 순서로 접근하기 시작했습니다. 미니게임들을 먼저 만들고 나중에 연결하는 것이 아니라, 먼저 하나의 세계를 만든 뒤 그 안에서 각각의 Arcade가 어떤 역할을 해야 하는지를 정하는 방식입니다. 세계를 만들고 나니 다음 질문은 공간이었습니다 이야기의 큰 흐름이 생기자 다음에는 그것을 실제 플레이 공간으로 바꿔야 했습니다. 어떤 사건이 어디에서 일어나는지, 플레이어는 어떤 순서로 이동하는지, 어떤 공간에서 무엇을 발견하는지, Arcade는 어느 지점에 등장하는지. 이제 이야기를 실제 게임의 Stage와 Map으로 변환하는 작업이 필요해졌습니다. 다음 글에서는 전체 이야기를 내부 원작처럼 먼저 만들어둔 이유와, 그 이야기를 어떤 방식으로 Mutation Arcade의 기준 자료로 사용했는지 조금 더 정리해보려고 합니다.
velog
(출처: arXiv:2609.36651, Figure 1 — 저자 공개 리포지토리 docs/assets/focusvtc_introduction.png) 논문 제목 : FocusVTC: Efficient and High-Performance Visual Text Compression with Adaptive Resolution 저자 : FangZhi Zhong, Xuerui Qiu, Yuqi Pan, Ya Liu, Shaowei Gu, Bo Xu, Guoqi Li (소속은 arXiv 초록 페이지·HF 페이퍼 카드·GitHub README 어디에도 표기되어 있지 않아 생략합니다.) 공개일 : 2026년 9월 29일 (arXiv 제출), Hugging Face Papers 등재 9월 30일 arXiv : https://arxiv.org/abs/2609.36651 (cs.CV) 코드 : https://github.com/fangzhi-zhong/FoucsVTC (리포지토리 이름의 Foucs 오타는 원본 그대로입니다) 모델 : https://huggingface.co/zfz04/FocusVTC (9B, BF16, base: Qwen/Qwen3.5-9B) 데이터셋 : REL-CoT — https://www.modelscope.cn/datasets/zhongfangzhi/REL-CoT 분류 태그 : Visual Text Compression · Document Understanding · Long-Context · Vision-Language · Tool Use 🔖 TL;DR (한눈에) 롱컨텍스트를 이미지로 압축하는 VTC(Visual Text Compression)의 고질적 딜레마 를 정면으로 다룹니다. 텍스트를 페이지 이미지로 렌더링하면 입력 토큰이 줄지만, 해상도를 고정해 두면 "저DPI = 토큰 절약 + 판독 불가" vs "고DPI = 판독 가능 + 무관 영역에 토큰 낭비"의 상충에서 벗어날 수 없습니다. FocusVTC의 답은 적응형 해상도(adaptive resolution) 입니다. 문서 전체는 저DPI 축소 페이지로 훑고, 근거가 있을 것 같은 영역만 zoom_region 도구로 고DPI 원본에서 잘라 와 추론 도중에 새로운 시각 관측으로 투입 합니다. 학습은 2단계입니다. 1 추론 과정을 페이지 번호·바운딩 박스에 묶은 REL-CoT 29.4K 데이터로 다해상도 REL-SFT, 2 도구 보조 GRPO 로 "언제 확대할지"와 "확대 결과를 어떻게 쓸지"를 강화학습. 별도의 continual-pretraining 단계는 없습니다. 성능: RULER v1(72 DPI)에서 2.9× 입력 압축으로 87.4점 — 같은 압축률대(3.0×)의 Glyph 57.5점을 크게 앞섭니다. LongBench에서는 텍스트 입력 백본보다 더 높은 56.40 (백본 55.86)을 기록했습니다. 압축 모델의 숙제인 일반 멀티모달 능력 퇴화도 일어나지 않았습니다 (MMMU 65.12 → 66.73, MME 2424.02 → 2457.62). MRCR 지연 평가에서는 텍스트 입력 대비 2.79× 온라인 종단간 속도 향상 . 한 줄 요약 : "문서를 통째로 고해상도로 읽을 필요 없다 — 작게 훑고, 필요한 데만 확대해서 읽으면 압축률과 정확도를 동시에 가져갈 수 있다"는 것을 9B VLM 하나로 실증한 연구. 📄 초록(Abstract) 한국어 번역 대형 언어 모델의 롱컨텍스트 추론은 상당한 연산량과 메모리를 요구한다. 시각 텍스트 압축(Visual Text Compression, VTC)은 텍스트를 이미지로 렌더링해 입력 길이를 줄이지만, 고정된 해상도로 렌더링하는 방식은 압축률과 성능 사이의 상충을 강제한다. 낮은 DPI는 토큰을 절약하는 대신 가독성을 희생하고, 높은 DPI는 가독성을 확보하는 대신 정작 무관한 내용에까지 토큰을 소모한다. 본 논문은 FocusVTC 를 제안한다. FocusVTC는 일반적인 멀티모달 능력을 보존하면서 적응형 해상도를 통해 이 상충을 깨뜨린다. 압축된 저DPI 전역 뷰를 선택적 영역 확대(selective region enhancement) 와 결합하고, 확대된 뷰를 진행 중인 추론 과정에 통합한다. 이를 위해 29.4K 규모의 고품질 Reasoning-Evidence Localization(REL) chain-of-thought 예제(REL-CoT) 를 구축하여 추론 흐름을 페이지 인덱스와 바운딩 박스에 연결한다. 다해상도 REL 지도 미세조정( REL-SFT )이 관련 영역을 지역화하도록 모델을 학습시키고, Group Relative Policy Optimization(GRPO) 이 언제 해상도를 높일 것인지와 그 결과로 얻은 관측을 어떻게 활용할 것인지를 학습하되, 별도의 continual-pretraining 단계를 두지 않는다 . RULER v1에서 72 DPI 기준, FocusVTC는 (도구 관측을 포함한) 2.9× 입력 압축에서 87.4점을 기록하며, 3.0× 입력 압축에서 57.5점인 Glyph를 능가한다. LongBench에서는 자신의 텍스트 입력 백본을 상회하고(56.40 대 55.86), MRCR 매크로 평균을 13.91점 향상시키며, VTCBench에서 51.19의 매크로 평균을 달성한다. MRCR 지연(latency) 평가에서는 텍스트 입력 대비 2.79× 온라인 종단간 속도 향상을 보인다. 또한 일반 멀티모달 능력이 보존되어 MMMU는 65.12에서 66.73으로, MME는 2424.02에서 2457.62로 상승한다. 요약 정리. VTC는 "긴 텍스트를 이미지로 바꿔서 토큰 수를 줄이자"는 접근인데, 지금까지는 렌더링 해상도를 한 번 정하면 그걸로 끝이었습니다. 이 논문은 그 고정 해상도 가정 자체를 버립니다. 저DPI로 전체를 보고, 근거가 있는 영역만 고DPI로 다시 들여다보는 능동적 읽기(active reading) 정책을 모델이 직접 학습합니다. 핵심 기여는 (1) 추론-근거-좌표를 하나의 CoT로 묶은 REL-CoT 데이터, (2) 다해상도 SFT + 도구 보조 GRPO의 2단계 레시피, (3) 압축률을 유지하면서 텍스트 백본과 일반 멀티모달 성능을 모두 지켜냈다는 실증입니다. 🧩 왜 이 문제가 중요한가 (배경) 롱컨텍스트 문제를 "픽셀로 푸는" 흐름은 지난 1년간 Document AI에서 가장 뜨거운 줄기였습니다. DeepSeek-OCR 계열이 "텍스트 토큰 대신 비전 토큰"이라는 아이디어를 대중화한 뒤로, 표 하나를 64토큰으로 압축하거나 레이아웃을 시각 토큰으로 접어 넣는 시도가 쏟아졌습니다. 발상은 단순하고 강력합니다. 텍스트 1,000토큰 분량의 한 페이지를 이미지 수백 토큰으로 넣을 수 있다면, 컨텍스트 윈도우와 KV 캐시를 그만큼 아낄 수 있다 는 것입니다. 그런데 실전에 쓰려고 하면 바로 벽을 만납니다. 압축률을 결정하는 변수가 사실상 렌더링 DPI 하나 인데, 이 값은 문서 전체에 일괄 적용됩니다. DPI를 낮추면 : 토큰은 확실히 줄어듭니다. 하지만 작은 글자, 각주, 표 셀 안의 숫자, 도면 레이블이 뭉개집니다. 바로 QA 정확도가 꺾이는 구간입니다. DPI를 높이면 : 글자는 읽힙니다. 그런데 100페이지 문서에서 정답과 관련된 부분이 두 문단뿐이라면, 나머지 98페이지를 또렷하게 렌더링하는 데 쓴 토큰은 전부 낭비입니다. 즉 압축률과 판독성이 전역적으로 묶여 있다 는 게 문제의 본질입니다. 사람이 두꺼운 보고서를 읽을 때를 생각해 보면 이상한 제약입니다. 우리는 목차와 페이지 레이아웃을 흐릿하게 훑다가, 숫자를 확인해야 할 때만 눈을 가까이 가져갑니다. 해상도를 전역 상수가 아니라 추론 중에 내리는 국소적 결정 으로 바꾸는 것 — FocusVTC가 겨냥하는 지점이 정확히 여기입니다. 이 변화는 단순한 효율 개선이 아닙니다. 모델이 "어디를 확대할지" 고르려면 근거의 위치를 알아야 하고, 근거 위치를 안다는 것은 곧 답의 출처를 좌표로 지목할 수 있다는 뜻입니다. 압축 연구가 자연스럽게 근거 귀속(evidence attribution) 연구와 만나는 셈이고, 이는 문서 QA를 실무에 올릴 때 가장 많이 요구받는 기능 중 하나입니다. 🔬 방법론 (출처: arXiv:2609.36651, 방법론 개요 그림 — 저자 공개 리포지토리 docs/assets/focusvtc_overview.png) 1) 기본 설정: 두 겹의 페이지 FocusVTC는 같은 문서를 두 가지 해상도로 동시에 준비 합니다. 공개된 평가 파이프라인 기준으로 저해상도 72 DPI / 고해상도 144 DPI 쌍입니다. 디스크 상에서도 dpi_72/ 와 dpi_144/ 두 트리로 나란히 유지되고, 두 트리의 파일명과 페이지 레이아웃이 정확히 일치 해야 합니다. 모델의 초기 프롬프트에 들어가는 것은 저해상도 페이지뿐 입니다. 고해상도 페이지는 디스크에 있지만 컨텍스트에는 없습니다 — 모델이 요청할 때만 지연 로딩(lazy load)됩니다. 이 설계가 압축률을 지켜 주는 핵심입니다. 렌더링 폰트는 DejaVu Sans 로 고정되어 있고, 학습 데이터 쪽은 더 넓게 48 / 60 / 72 / 84 / 96 / 120 / 144 — 7가지 DPI 뷰 를 모두 렌더링합니다. 한 문서의 모든 DPI 변형이 train/val 중 한쪽에만 들어가도록 metadata.base_id 기준으로 분할하는데, 이는 같은 내용이 해상도만 바꿔 양쪽에 걸치는 누출을 막기 위한 장치입니다. 2) zoom_region: 추론 도중에 해상도를 올리는 도구 모델이 쓰는 도구는 딱 하나입니다. { "name": "zoom_region", "description": "Crop a potentially unreadable region from a document page and return it as a new image.", "parameters": { "page": { "type": "integer", "description": "1-based page number." }, "bbox_2d": { "type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4, "description": "[x1, y1, x2, y2] normalized to [0, 1000]." } } } 인터페이스가 의도적으로 단순합니다. 1-based 페이지 번호 와 [0, 1000]으로 정규화된 바운딩 박스 네 값만 내놓으면, 런타임이 해당 저DPI 페이지에 대응하는 고DPI 페이지( dpi_72/page_k → dpi_144/page_k )를 찾아 그 영역을 잘라 다음 턴의 시각 관측으로 되돌려 줍니다 . 여기서 중요한 포인트 두 가지입니다. 좌표 정규화가 해상도 독립성을 만듭니다. [0, 1000] 스케일로 말하기 때문에, 모델은 저DPI 뷰에서 본 위치를 그대로 고DPI 좌표계로 옮길 수 있습니다. 모델이 두 해상도의 픽셀 치수를 알 필요가 없습니다. 크롭은 모델이 아니라 호스트 런타임이 수행합니다. 모델 카드가 명시적으로 경고하듯, 순수 transformers.generate() 만으로는 도구가 실행되지 않습니다. 도구 호출을 파싱하고 박스를 검증하고 크롭을 수행해 다시 붙여 주는 외부 게이트웨이가 반드시 필요합니다. 짝이 되는 고해상도 페이지가 없으면 명시적으로 실패한 도구 관측 이 되돌아옵니다(조용히 저해상도 이미지로 대체하지 않습니다). 평가 시 기본 정책은 최대 8회 줌 호출, 총 9턴, 턴당 생성 2,048토큰, 전체 트래젝토리 10,240토큰 입니다. 이 트래젝토리 예산에는 생성 텍스트뿐 아니라 되돌아온 크롭 이미지의 토큰까지 포함 해서 셉니다 — 즉 "확대해서 읽은 비용"이 압축률 계산에 정직하게 반영됩니다. 초록이 압축률을 보고할 때 "도구 관측을 포함한 2.9×"라고 단서를 붙인 이유입니다. 3) REL-CoT: 추론을 좌표에 묶는 데이터 적응형 읽기를 학습시키려면 "이 질문의 근거는 몇 페이지 어디에 있다"는 지도가 필요합니다. 저자들이 만든 것이 REL-CoT(Reasoning-Evidence Localization chain-of-thought) , 규모는 29.4K 예제 입니다. 구조적으로 각 예제는 추론 트레이스를 근거 페이지 인덱스 + 바운딩 박스 에 연결합니다. 좌표 규약은 도구와 동일하게 1-based 페이지, [0, 1000] 정규화 박스 로 통일되어 있습니다. 학습 타깃은 <think>...</think> 블록과 답변 텍스트를 포함하는 형태입니다. 데이터 구축 방식에서 눈여겨볼 점은, 공개된 렌더러가 새로운 지도 신호를 모델로 생성하지 않는다 는 것입니다. 원문 텍스트를 7가지 DPI로 다시 렌더링하면서 기존의 추론과 정답은 보존하고, 근거 박스와 페이지 참조만 새 레이아웃에 맞게 재매핑 합니다. 공개 범위 문서에서도 "자동 REL 주석 생성과 합성 학습 태스크 생성"은 릴리스에 포함되지 않는다고 분명히 적어 두었습니다. 즉 좌표 재매핑이 곧 해상도 증강 인 구조입니다. 같은 근거가 48 DPI에서는 어떻게, 144 DPI에서는 어떻게 보이는지를 동일한 의미 라벨 아래에서 학습하게 됩니다. 4) 1단계 — REL-SFT (다해상도 지도 미세조정) 백본은 Qwen3.5-9B 입니다. 이 단계의 목표는 "관련 영역을 지역화하는 능력"을 심는 것입니다. 공개 레시피에서 확인되는 설정들: 비전 인코더( model.visual )는 동결(freeze) 합니다. 시각 표현은 건드리지 않고 언어 측의 지역화·추론 능력을 올립니다. 랜덤 그라운딩 프롬프트 를 활성화해 좌표 지시 형식에 과적합되지 않게 합니다. 32K 토큰 패킹 으로 샘플을 묶고, FSDP2 로 분산 학습합니다. 패킹은 mRoPE 위치와 어텐션/DeltaNet/컨볼루션의 시퀀스 경계를 함께 공급합니다. Qwen3.5의 하이브리드 어텐션 구현 때문에 use_rmpad: false , sp_ulysses_degree: 1 이 요구됩니다. 기준 구성은 8 GPU 단일 노드 입니다. 다해상도 데이터를 한 매니페스트에 섞어 학습하므로, 모델은 "저DPI에서 뭉개진 글자를 보고도 그게 어디쯤 무슨 내용인지 추정하는" 감각을 갖게 됩니다. 이 감각이 곧 "어디를 확대해야 하는지" 판단의 기반이 됩니다. 5) 2단계 — 도구 보조 GRPO (언제·어떻게 확대할지 학습) SFT만으로는 부족합니다. 지역화를 배운다고 해서 호출 타이밍과 호출 효율 이 좋아지지는 않습니다. 쓸데없이 8번 다 쓰거나, 정작 필요할 때 안 쓰거나, 페이지 전체를 박스로 잡아 압축 이득을 날려버릴 수 있습니다. 이를 GRPO(Group Relative Policy Optimization) 로 교정합니다. 보상 설계 가 이 논문에서 가장 음미할 부분입니다. 보상은 세 축으로 구성됩니다. 보상 구성 요소 역할 답변 정확도(answer accuracy) 최종 정답의 정합성 구조적 포맷(structural format) <think> /도구 호출 스키마 등 출력 형식 준수 도구 품질(tool quality) 확대 행위 자체의 효율성 핵심 장치는 도구 보너스가 "완전히 정답일 때만" 열린다(gated on a fully correct answer) 는 점입니다. 틀린 답을 내면서 도구를 예쁘게 쓰는 것으로는 보상을 얻을 수 없습니다. 도구 사용이 목적이 되는 리워드 해킹을 원천 차단 하는 설계입니다. 그리고 열린 도구 보너스는 다음 네 가지에 의존합니다. 근거 IoU — 잡은 박스가 실제 근거와 얼마나 겹치는가 크롭 면적 — 필요한 만큼만 작게 잡았는가 (페이지 통째로 잡으면 압축 이득이 사라집니다) DPI — 해상도 상승폭. 네이티브 144→144 학습 케이스에서는 DPI 보너스가 0 으로 정의됩니다(올릴 해상도가 없으므로 공짜 점수를 주지 않습니다) 중복 호출 — 같은 영역을 반복 호출하는 낭비에 대한 페널티 근거 주석이 비어 있거나 없는 샘플은 근거 중첩 보너스를 받을 수 없습니다 . 즉 좌표 라벨이 있는 데이터에만 지역화 보상이 걸립니다. 학습 측 하이퍼파라미터(공개 기본값): 항목 값 프롬프트 배치 / PPO 미니배치 8 / 8 프롬프트당 롤아웃 수 4 PPO 마이크로배치 (GPU당) 1 초기 프롬프트 예산 8,192 토큰 응답 + 도구 관측 예산 10,240 토큰 총 에폭 1 저장·검증 주기 50 스텝 상호작용 예산 도구 호출 8회 + 최종 답변 1턴 학습률 문서상 예시 오버라이드로 5e-7 제시 (기본값으로 명시되진 않음) 인프라는 Ray 워커 + FSDP + 수정된 vLLM SPMD 백엔드이고, 기준 레시피는 역시 대용량 메모리 8 GPU 단일 노드를 가정합니다. 참조 환경은 PyTorch 2.10.0 / Transformers 5.16.1 / vLLM 0.19.1 / FlashAttention 2.8.3입니다. 마지막으로 학습과 평가의 정합성 을 챙긴 디테일이 하나 있습니다. 평가 게이트웨이가 GRPO 환경과 도구 파서, 박스 검증, 크롭 기하를 공유 하고, 커스텀 Qwen3.5 챗 템플릿이 과거 도구 턴의 reasoning을 보존 합니다. GRPO의 누적 토큰 트래젝토리와 추론 시 동작이 어긋나지 않게 맞춘 것입니다. 도구 사용 에이전트에서 train/serve skew는 흔한 함정인데, 이 부분을 명시적으로 처리했습니다. 📊 실험 결과 초록에서 보고된 수치를 정리하면 다음과 같습니다. (상세 표는 arXiv 본문에 있으며, 본 리뷰는 초록·모델 카드·공개 리포지토리에서 확인 가능한 수치만 사용했습니다.) 롱컨텍스트 벤치마크 벤치마크 조건 FocusVTC 비교 대상 RULER v1 72 DPI, 2.9× 입력 압축(도구 관측 포함) 87.4 Glyph 57.5 (3.0× 입력 압축) LongBench — 56.40 텍스트 입력 백본 55.86 MRCR 매크로 평균 +13.91점 향상 (기준 대비) VTCBench 매크로 평균 51.19 — RULER v1이 가장 강한 결과입니다. 압축률은 사실상 동급(2.9× vs 3.0×)인데 점수가 87.4 대 57.5로 29.9점 벌어집니다. 고정 해상도 VTC가 72 DPI 구간에서 겪는 판독성 붕괴를, 선택적 확대가 거의 전부 복구한다고 읽을 수 있습니다. 여기서 압축률에 도구로 되돌아온 크롭 이미지 토큰까지 포함 시켰다는 점이 중요합니다. "확대하느라 쓴 토큰을 빼고 계산한 압축률"이 아니라는 뜻이니, 비교가 공정한 쪽으로 기울어 있습니다. LongBench 결과는 성격이 다릅니다. 56.40 대 55.86, 차이는 0.54점으로 작습니다. 하지만 비교 대상이 같은 모델의 텍스트 입력 버전 이라는 게 핵심입니다. 압축 연구에서 보통 기대하는 최선은 "원본 텍스트 성능에 근접"인데, 여기서는 그 선을 넘었습니다 . 전역 저해상도 뷰가 레이아웃 정보를 함께 주는 효과, 그리고 확대 과정이 일종의 명시적 근거 집중으로 작동하는 효과가 압축 손실을 상쇄했다고 해석할 수 있습니다. 다만 0.54점은 작은 마진이므로, 이를 "압축이 텍스트보다 낫다"로 일반화하기보다 "최소한 손해는 아니다"로 읽는 쪽이 안전합니다. MRCR 매크로 평균 +13.91점 은 다중 참조 해결(multi-round co-reference) 과제에서 적응형 확대의 이득이 크다는 신호입니다. 긴 대화·문서 속에서 특정 지점을 다시 찾아 확인해야 하는 과제 성격과 zoom_region 의 작동 방식이 잘 맞습니다. 효율 지표 결과 MRCR 온라인 종단간 속도 텍스트 입력 대비 2.79× RULER v1 입력 압축률 2.9× (도구 관측 포함) 속도 이득이 압축률(2.9×)과 거의 같은 배율(2.79×)로 나타난다는 게 깔끔합니다. 토큰을 줄인 만큼 실제 지연으로 돌려받았다는 뜻이고, 도구 호출로 인한 멀티턴 오버헤드가 압축 이득을 잡아먹지 않았다는 증거이기도 합니다. 일반 멀티모달 능력 보존 벤치마크 학습 전 FocusVTC MMMU 65.12 66.73 (+1.61) MME 2424.02 2457.62 (+33.60) 이 표가 생각보다 중요합니다. 문서 특화 미세조정을 세게 하면 일반 VQA·추론 능력이 깎이는 게 흔한 부작용인데(이른바 특화 세금), FocusVTC는 두 지표 모두 오히려 소폭 상승 했습니다. 비전 인코더를 동결하고, 랜덤 그라운딩 프롬프트로 형식 과적합을 막고, 초록이 강조하듯 별도 continual-pretraining 단계를 두지 않은 설계 선택이 여기서 값을 하는 것으로 보입니다. 상승폭 자체는 크지 않으니 "향상됐다"보다 "퇴
Балл: 54.39Уверенность: 49%
Балл: 54.39Уверенность: 49%
Балл: 54.38Уверенность: 49%
Балл: 54.38Уверенность: 49%
Балл: 54.37Уверенность: 49%
Балл: 54.37Уверенность: 49%
Балл: 54.37Уверенность: 49%