문서 작성 AI 서비스 만들기

2025. 7. 2. 23:26MS AI School

다들 가끔 공공기관 등에 제출해야 하는 서류(신청서, 증명서 등)를 작성할때, 매우 스트레스를 받았던 경험이 한 번씩은 있을 것이다.

예를 들어, 전입 신고를 해야 한다고 가정을 해보자.

 

그럼 먼저 전입 신고시 필요한 서류들이 어떤 것이 있는지 찾아야 할 것이고, 해당 서류들을 어디서 뗄 수 있는지, 양식을 받아와서 본인이 작성을 해야 하는 서류들도 있을 것이다.

우리도 이런 일들이 있을때 귀찮고 번거로운데, 우리나라에 사는 외국인, 또는 나이 드신 노인분들은 얼마나 힘드실까?

이런 생각에 신청서, 문서 등을 작성할 수 있데 도와주는 AI 서비스를 만들어보게 되었다.

 

소규모로 간단하게 몇 시간 동안 초안 정도로만 만들어본거라 아직 엄청 거창한 서비스를 개발한 것은 아니지만, 추후에 충분히 확장이 가능한 주제라고 생각한다.

 

Azure의 Document Intelligence는 문서, 이미지 등을 분석하고 처리할 수 있도록 해주는 클라우드 기반 서비스이다.

이 Document Intelligence 와 이전 포스팅에서도 다뤘던 OpenAI를 함께 사용하여 개발했다.

 

Azure Document Intelligene 사용 방법

 

해당 리소스를 만든 후 Studio로 이동하면 아래와 같은 여러 분석 모델들이 있다.

 

대부분의 모델별 사용법은 다 비슷해서, 대표적으로 OCR/Read 를 사용하여 로컬에서 구현해보았다.

 

로컬에서 구현하기 전, 포털에서 기능을 먼저 사용해보면, 문서의 각 텍스트들을 추출하여 분석을 한 결과를 볼 수 있다.

 

하나의 이미지는 물론이고, 여러장의 PDF를 분석하는 것도 가능하다.

 

해당 모델을 로컬에서 호출하여 사용하려면 Endpoint와 API Key 등이 필요하다.

자세한 방법은 Azure 공식 문서를 참고 하였다.

https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&pivots=programming-language-rest-api

 

빠른 시작: 문서 인텔리전스 클라이언트 라이브러리 - Azure AI services

문서 인텔리전스 SDK 또는 REST API를 사용하여 문서에서 주요 데이터 및 구조 요소를 추출하는 양식 처리 앱을 만듭니다.

learn.microsoft.com

 

공식 문서를 보면 알 수 있듯이, Endpoint와 Key값은 Document Intelligence 리소스에 가면 확인할 수 있다.

Document Intelligence 리소스

 

OCR 결과와 PDF 필드를 GPT에 전송

 

기존에 만들려고 생각했던 것은 사용자가 "OO신청서를 작성하려고 해" 라고 요청을 하면, 보유하고 있는 여러 양식 파일들 중 요청에 맞는 파일을 불러와서 질의응답을 진행하는 것이었다.

하지만 지금 당장은 간단한 서비스를 만들어보는 것이기에, 사용자가 작성해야 할 문서 파일을 업로드 하면, 해당 파일 OCR을 진행하고, 필요한 정보에 대한 질문을 사용자에게 하는 것으로 구현을 했다.

 

사용자가 파일을 업로드 하면, 해당 파일을 Document Intelligence의 OCR/Read 모델로 텍스트 추출을 진행한다.

추출된 텍스트와 PDF 추출 필드를 모두 GPT에 전송하여, GPT는 사용자로 부터 받아야 하는 정보(예 : 이름, 주소, 전화번호 등)에 대한 질문 리스트를 생성하게 된다.

def handle_pdf_upload(file_path):
    global field_list, field_index, field_answers, inferred_answers, uploaded_pdf_path, pdf_fields
    
    if not file_path:
        return "PDF 파일을 선택해주세요.", None
    
    print(f"PDF 업로드됨: {file_path}")
    uploaded_pdf_path = file_path
    
    # PDF 필드명 추출
    pdf_fields = get_pdf_fields(file_path)
    print(f"추출된 PDF 필드들: {pdf_fields}")
    
    # OCR 텍스트 추출
    ocr_text = extract_text_from_pdf(file_path)
    if not ocr_text:
        return "OCR 텍스트 추출에 실패했습니다. 다른 PDF 파일을 시도해보세요.", None
    
    field_list = generate_questions_from_text(ocr_text, pdf_fields)
    if not field_list:
        return "질문 생성에 실패했습니다.", None
    
    field_index = 0
    field_answers = {}
    inferred_answers = {} 
    
    fields_info = f"발견된 PDF 필드: {len(pdf_fields)}개\n" if pdf_fields else "PDF 필드를 감지하지 못했습니다. 텍스트 기반으로 진행합니다.\n"
    
    first_question_text, _ = field_list[0]
    
    return f"문서 분석이 완료되었습니다.\n{fields_info}총 {len(field_list)}개의 질문이 있습니다.\n\n첫 번째 질문: {first_question_text}", None

 

GPT가 사용자에게 질문을 할때, 기존에 답변할 때 처럼 장황하고 길게 한다면, 사용자가 질문을 제대로 이해할 수가 없거나, 올바른 답변을 할 수 없을 수 있기 때문에 최대한 간결하게 필요한 질문만 하도록 나의 의도를 Gemini에게 공유하여 함께 프롬프트를 작성하였다.

 prompt = f"""
다음은 문서에서 OCR로 추출한 텍스트와 PDF의 실제 필드명들입니다.
사용자가 입력해야 할 항목들을 파악하고 질문을 생성해주세요.

문서 내용:
{ocr_text}

PDF 필드명들:
{fields_info}

요구사항:
1. 문서의 빈칸이나 입력 필드를 분석하여 적절한 질문을 생성하세요
2. 각 질문은 명확하고 이해하기 쉽게 작성하세요
3. 한 줄에 하나의 질문만 작성하세요
4. 질문 앞에 번호나 특수문자를 붙이지 마세요
5. **각 질문은 해당하는 PDF 필드명을 매핑해야 합니다. 만약 PDF 필드명에 직접적으로 매칭되는 정보가 없으면 'auto_infer'라고 필드명을 지정하세요.**
6. 형식: "질문내용|필드명" (예: "이름을 입력해주세요|name_field" 또는 "생년월일을 입력해주세요|auto_infer")

질문들을 생성해주세요:
"""

 

답변에 대한 AI 모델 추론

 

문서 양식의 모든 빈칸에 대해서 하나씩 다 질문을 한다면, 본인이 직접 작성하는 것이랑 별로 다를게 없지 않을까?

그래서 나는 사용자가 답변한 정보들은 모델이 모두 저장을 하고, 추후에 또 새로운 문서를 작성하게 됐을때, 모델에 저장되어 있는 데이터에 대한 정보는 모델이 직접 다 입력을 하고, 더 나아가 추론을 통해 작성할 수 있는 란도 모두 AI가 다 작성을 할 수 있게 하는 것이 목표였다.( 예 : 사용자가 "현재 직장명이 무엇인가요?"라는 질문에 답을 하였음 -> 나중에 현재 재직 상태를 묻는 란이 있다면 그 칸은 당연히 "재직중" 이라고 답을 할 수 있을 것이다)

 

그런데 내가 아무리 테스트를 해봐도, 비슷한 질문에 대해서 자동으로 기입을 해주질 않길래 뭐가 잘못 됐는지 찾아봤는데, 아직 정확한 원인은 찾지 못했다...

아마 Azure 서비스의 토큰 제한 때문인걸로 추정을 하고 있지만, 정확한 원인은 추후 더 개발을 하면서 찾아보려고 한다.

def handle_user_text(user_text):
    global field_index, field_list, field_answers, inferred_answers, uploaded_pdf_path, pdf_fields

    if not field_list and not pdf_fields:
        return "먼저 PDF 파일을 업로드하고 분석을 시작해주세요.", None

    if field_index >= len(field_list):
        if uploaded_pdf_path:
            final_field_values = map_answers_to_fields(field_list, field_answers, inferred_answers)
            filled_pdf = generate_filled_pdf(uploaded_pdf_path, final_field_values)
            if filled_pdf:
                return "모든 항목이 완료되었습니다! 작성된 문서를 다운로드하세요.", filled_pdf
            else:
                return "문서 생성 중 오류가 발생했습니다.", None
        else:
            return "원본 PDF 파일을 찾을 수 없습니다.", None

    current_question_info = field_list[field_index]
    current_question_text, current_field_name = current_question_info
    
    if not user_text or not user_text.strip():
        return f"답변을 입력해주세요.\n현재 질문: {current_question_text}", None
    else:
        field_answers[current_question_text] = user_text.strip()
        field_index += 1
        print(f"사용자 답변 저장: {current_question_text} -> {user_text.strip()}")
        print(f"현재 진행상황 (사용자 질문): {field_index}/{len(field_list)}")


    inferred_count_this_turn = 0 
    if uploaded_pdf_path and pdf_fields:

        previous_inferred_keys = set(inferred_answers.keys())

        current_mapped_user_answers = map_answers_to_fields(field_list, field_answers, {}) 
        ocr_text_for_inference = extract_text_from_pdf(uploaded_pdf_path) 
        
        inferred_answers_new = request_gpt_inference(
            ocr_text_for_inference, 
            pdf_fields, 
            current_mapped_user_answers 
        )
        inferred_answers.update(inferred_answers_new) 
        print(f"현재 AI 추론된 답변: {inferred_answers}")

        # 새로 추론된 필드 개수 계산
        current_inferred_keys = set(inferred_answers.keys())
        newly_inferred_keys = current_inferred_keys - previous_inferred_keys
        inferred_count_this_turn = len(newly_inferred_keys)
        print(f"이번 턴에 새로 추론된 필드 개수: {inferred_count_this_turn}")


    # --- 다음 질문이 있는지 확인 및 AI 자동 건너뛰기 로직 ---
    response_message = "답변이 저장되었습니다."
    if inferred_count_this_turn > 0:
        response_message += f"\n\n👉 AI가 추론을 통해 {inferred_count_this_turn}개의 질문에 대한 답변을 추가로 작성하였습니다."

    while field_index < len(field_list):
        next_question_text, next_field_name = field_list[field_index]
        
        if next_question_text in field_answers or \
           (next_field_name in pdf_fields and next_field_name in inferred_answers):
            print(f"질문 '{next_question_text}' (필드: {next_field_name})은(는) 이미 처리되었으므로 건너뜁니다.")
            field_index += 1
        else:
            # 아직 답변되지 않은 (사용자 질문이 필요한) 질문이 남아있음
            return f"{response_message}\n\n다음 질문: {next_question_text}", None

    # 모든 질문이 완료되었거나 AI가 모두 채울 수 있어서 더 이상 사용자 질문이 필요 없는 경우
    if uploaded_pdf_path:
        final_field_values = map_answers_to_fields(field_list, field_answers, inferred_answers)
        filled_pdf = generate_filled_pdf(uploaded_pdf_path, final_field_values)
        if filled_pdf:
            return "모든 항목이 완료되었습니다! 작성된 문서를 다운로드하세요.", filled_pdf
        else:
            return "문서 생성 중 오류가 발생했습니다.", None
    else:
        return "원본 PDF 파일을 찾을 수 없습니다.", None

 

Gradio 화면 구성

 

개발을 하다보니까 기능 하나하나를 추가 할때마다 오류가 발생해서, 기존에는 STT, TTS 기능을 모두 탑재 했었지만, 현재는 빠져있는 상태이다...

with gr.Blocks(title="문서 작성 도우미") as demo:
    gr.Markdown("# 문서 자동 작성 도우미")
    gr.Markdown("PDF 파일을 업로드하면 AI가 빈칸을 파악하고 질문을 통해 문서를 작성해드립니다.")
    
    with gr.Row():
        with gr.Column():
            pdf_file = gr.File(label="PDF 업로드", file_types=[".pdf"])
            upload_button = gr.Button("📄 분석 시작", variant="primary")
            
        with gr.Column():
            chatbot_text = gr.Textbox(
                label="AI 질문", 
                interactive=False, 
                lines=4,
                placeholder="PDF를 업로드하고 분석을 시작하면 질문이 표시됩니다."
            )
    
    with gr.Row():
        user_text_input = gr.Textbox(
            label="답변 입력", 
            placeholder="여기에 답변을 입력하고 Enter를 누르세요.",
            lines=2
        )
        submit_button = gr.Button("답변 제출", variant="secondary")
    
    with gr.Row():
        download_link = gr.File(label="📋 작성된 PDF 다운로드", interactive=False)
    
    # 이벤트 연결
    upload_button.click(
        handle_pdf_upload, 
        inputs=[pdf_file], 
        outputs=[chatbot_text, download_link]
    )
    
    user_text_input.submit(
        handle_user_text, 
        inputs=[user_text_input], 
        outputs=[chatbot_text, download_link]
    ).then(
        lambda: "", 
        outputs=[user_text_input]
    )
    
    submit_button.click(
        handle_user_text, 
        inputs=[user_text_input], 
        outputs=[chatbot_text, download_link]
    ).then(
        lambda: "", 
        outputs=[user_text_input]
    )

if __name__ == "__main__":
    demo.launch(allowed_paths=["."])

 

결과

이런 식으로 AI와 사용자는 질문을 주고 받고, 최종 완성된 양식의 파일을 다운받을 수 있게 제공해준다.

아직 개발 중인 단계라 조금은(?) 삐걱 거리는 곳이 많고, 완성도도 현저히 떨어지지만, 내가 생각해낸 아이디어가 너무 좋아서 조금은 급하게 포스팅을 하게 되었다.

 

나중에 수정이 되고, 업그레이드 되어서 완전한 모습을 갖추게 된다면 다시 포스팅을 할 예정이다.