From 9aa4e0a343e13b465e17d36f804d6c3e8f48baee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Mon, 7 Sep 2026 11:31:32 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20EASONING=5FEFFORT=20=3D=20"low"=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/autotag.py | 10 ++++++--- app/utils/prompts/chatgpt_prompt.py | 32 +++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/utils/autotag.py b/app/utils/autotag.py index 3ccade6..3ff9653 100644 --- a/app/utils/autotag.py +++ b/app/utils/autotag.py @@ -6,6 +6,10 @@ from app.utils.prompts.schemas import SpaceType, Subject, Camera, MotionRecommen import asyncio +# medium 추론은 출력 비용의 80%를 차지하고, minimal은 A/B 비교(4회)에서 narrative 점수가 +# welcome 단계로 편향되고 태그를 과다 선택하는 패턴이 반복돼 low로 고정한다. +IMAGE_TAG_REASONING_EFFORT = "low" + async def autotag_image(image_url : str, industry: str = "") -> list[str]: #tag_list chatgpt = ChatgptService(model_type="gpt") image_input_data = { @@ -17,7 +21,7 @@ async def autotag_image(image_url : str, industry: str = "") -> list[str]: #tag_ "motion_recommended" : list(MotionRecommended) } - image_result = await chatgpt.generate_structured_output(image_autotag_prompt, image_input_data, image_url, False) + image_result = await chatgpt.generate_structured_output(image_autotag_prompt, image_input_data, image_url, True, reasoning_effort=IMAGE_TAG_REASONING_EFFORT) return image_result async def autotag_images(image_url_list : list[str], industry: str = "") -> list[dict]: #tag_list @@ -31,7 +35,7 @@ async def autotag_images(image_url_list : list[str], industry: str = "") -> list "motion_recommended" : list(MotionRecommended) }for image_url in image_url_list] - image_result_tasks = [chatgpt.generate_structured_output(image_autotag_prompt, image_input_data, image_input_data['img_url'], False, silent = True) for image_input_data in image_input_data_list] + image_result_tasks = [chatgpt.generate_structured_output(image_autotag_prompt, image_input_data, image_input_data['img_url'], True, silent = True, reasoning_effort=IMAGE_TAG_REASONING_EFFORT) for image_input_data in image_input_data_list] image_result_list: list[BaseModel | BaseException] = await asyncio.gather(*image_result_tasks, return_exceptions=True) MAX_RETRY = 2 for _ in range(MAX_RETRY): @@ -40,7 +44,7 @@ async def autotag_images(image_url_list : list[str], industry: str = "") -> list if not failed_idx: break retried = await asyncio.gather( - *[chatgpt.generate_structured_output(image_autotag_prompt, image_input_data_list[i], image_input_data_list[i]['img_url'], False, silent=True) for i in failed_idx], + *[chatgpt.generate_structured_output(image_autotag_prompt, image_input_data_list[i], image_input_data_list[i]['img_url'], True, silent=True, reasoning_effort=IMAGE_TAG_REASONING_EFFORT) for i in failed_idx], return_exceptions=True ) for i, result in zip(failed_idx, retried): diff --git a/app/utils/prompts/chatgpt_prompt.py b/app/utils/prompts/chatgpt_prompt.py index 893a8f9..7e8c06a 100644 --- a/app/utils/prompts/chatgpt_prompt.py +++ b/app/utils/prompts/chatgpt_prompt.py @@ -46,7 +46,20 @@ class ChatgptService: ) case _: raise NotImplementedError(f"Unknown Provider : {model_type}") - + + def _log_usage(self, response, model: str, output_format: type[BaseModel]) -> None: + usage = getattr(response, "usage", None) + if usage is None: + return + # 토큰 소모량 로깅 (필요 시 주석 해제) + # cached = getattr(getattr(usage, "prompt_tokens_details", None), "cached_tokens", None) or 0 + # reasoning = getattr(getattr(usage, "completion_tokens_details", None), "reasoning_tokens", None) or 0 + # logger.info( + # f"[ChatgptService({self.model_type})] usage model={model} output={output_format.__name__} " + # f"prompt={usage.prompt_tokens} cached={cached} " + # f"completion={usage.completion_tokens} reasoning={reasoning} total={usage.total_tokens}" + # ) + async def _call_pydantic_output( self, prompt : str, @@ -113,9 +126,10 @@ class ChatgptService: self, prompt : str, output_format : BaseModel, #입력 output_format의 경우 Pydantic BaseModel Class를 상속한 Class 자체임에 유의할 것 - model : str, + model : str, img_url : str, - image_detail_high : bool) -> BaseModel: + image_detail_high : bool, + reasoning_effort : Optional[str] = None) -> BaseModel: content = [] if img_url: content.append({ @@ -129,13 +143,16 @@ class ChatgptService: "type": "text", "text": prompt }) + # gpt-5.4 계열/Gemini 호환 엔드포인트는 허용 값이 다르거나 파라미터를 거부하므로 지정된 경우에만 전달 + extra_kwargs = {"reasoning_effort": reasoning_effort} if reasoning_effort else {} last_error = None for attempt in range(self.max_retries + 1): try: response = await self.client.beta.chat.completions.parse( model=model, messages=[{"role": "user", "content": content}], - response_format=output_format + response_format=output_format, + **extra_kwargs, ) except (ValidationError, json.JSONDecodeError) as e: # 모델이 스키마에 맞지 않는 JSON을 반환한 경우 (예: trailing characters). @@ -148,6 +165,7 @@ class ChatgptService: if attempt < self.max_retries: logger.info(f"[ChatgptService({self.model_type})] Retrying request...") continue + self._log_usage(response, model, output_format) # Response 디버그 로깅 # logger.debug(f"[ChatgptService({self.model_type})] attempt: {attempt}") # logger.debug(f"[ChatgptService({self.model_type})] Response ID: {response.id}") @@ -225,6 +243,7 @@ class ChatgptService: continue raise last_error + self._log_usage(response, model, output_format) choice = response.choices[0] if choice.finish_reason == "stop": return choice.message.parsed @@ -242,7 +261,8 @@ class ChatgptService: input_data : dict, img_url : Optional[str] = None, img_detail_high : bool = False, - silent : bool = True + silent : bool = True, + reasoning_effort : Optional[str] = None, ) -> BaseModel: prompt_text = prompt.build_prompt(input_data, silent) @@ -253,5 +273,5 @@ class ChatgptService: # GPT API 호출 #parsed = await self._call_structured_output_with_response_gpt_api(prompt_text, prompt.prompt_output, prompt.prompt_model) # parsed = await self._call_pydantic_output(prompt_text, prompt.prompt_output_class, prompt.prompt_model, img_url, img_detail_high) - parsed = await self._call_pydantic_output_chat_completion(prompt_text, prompt.prompt_output_class, prompt.prompt_model, img_url, img_detail_high) + parsed = await self._call_pydantic_output_chat_completion(prompt_text, prompt.prompt_output_class, prompt.prompt_model, img_url, img_detail_high, reasoning_effort) return parsed \ No newline at end of file