Recientemente, los usuarios de X y Reddit informaron que escribir código con Claude 3.5 Sonnet los hizo sentir "muy poderosos". Una de esas publicaciones que llamó específicamente la atención fue un tweet del director ejecutivo de YCombinator, Garry Tan.
Garry compartió la siguiente publicación de uno de los miembros de la comunidad YCombinator Reddit sobre cómo el uso de Claude 3.5 Sonnet aumentó su productividad 10 veces al implementar funciones populares.
La publicación también destacó la importancia de tomar decisiones arquitectónicas e infraestructurales acertadas como parte crucial de su uso diario de LLM.
Aunque los LLM como Claude 3.5 ofrecen beneficios sobresalientes, todavía tienen limitaciones con respecto al mantenimiento de la memoria y el contexto. Se pueden utilizar varias estrategias de desarrollo para abordar estas limitaciones. Estas estrategias giran en torno a los fundamentos del desarrollo de software que todos los desarrolladores perfeccionan con experiencia, pero que a menudo son fáciles de pasar por alto cuando se solicitan LLM en inglés sencillo.
La aplicación de los mismos principios básicos que los desarrolladores utilizan a diario, lo que el autor de Reddit denomina decisiones arquitectónicas, a las interacciones LLM puede dar como resultado un código altamente modular, escalable y bien documentado.
Los siguientes son algunos principios de codificación y prácticas de desarrollo clave que se pueden aplicar al desarrollo de software asistido por LLM, junto con ejemplos prácticos en Python:
Todas las indicaciones de los ejemplos se utilizaron con Claude Sonnet 3.5.
Para que sea más fácil describir cada componente lógico en el LLM y obtener componentes precisos, divida el código base en componentes pequeños y bien definidos.
# database.py class Database: def __init__(self, sql_connection_string): .... def query(self, sql): .... # user_service.py class UserService: def __init__(self, database): self.db = database def get_user(self, user_id): return self.db.query(f"SELECT * FROM users WHERE id = {user_id}") # main.py db = Database("sql_connection_string") user_service = UserService(db) user = user_service.get_user(123)
Para ocultar implementaciones complejas, utilice capas de abstracción y concéntrese en abstracciones de nivel superior utilizando los detalles de cada componente registrados a lo largo del camino.
# top-level abstraction class BankingSystem: def __init__(self): self._account_manager = AccountManager() self._transaction_processor = TransactionProcessor() def create_account(self, acct_number: str, owner: str) -> None: self._account_manager.create_account(acct_number, owner) def process_transaction(self, acct_number: str, transaction_type: str, amount: float) -> None: account = self._account_manager.get_account(acct_number) self._transaction_processor.process(account, transaction_type, amount) # mid-level abstractions class AccountManager: def __init__(self): def create_account(self, acct_number: str, owner: str) -> None: def get_account(self, acct_number: str) -> 'Account': class TransactionProcessor: def process(self, account: 'Account', transaction_type: str, amount: float) -> None: # lower-level abstractions class Account(ABC): .... class Transaction(ABC): .... # concrete implementations class SavingsAccount(Account): .... class CheckingAccount(Account): .... class DepositTransaction(Transaction): .... class WithdrawalTransaction(Transaction): .... # lowest-level abstraction class TransactionLog: .... # usage focuses on the high-level abstraction cart = ShoppingCart() cart.add_item(Item("Book", 15)) cart.add_item(Item("Pen", 2)) total = cart.get_total()
Para hacer que las tareas sean más manejables cuando busque ayuda de LLM, defina interfaces claras para cada componente y concéntrese en implementarlas todas por separado.
class PaymentProcessor(ABC): @abstractmethod def process_payment(self, amount: float, card_no: str) -> bool: .... class StripeProcessor(PaymentProcessor): # stripe specific implementation def process_payment(self, amount: float, card_no: str) -> bool: .... class PayPalProcessor(PaymentProcessor): # paypal specific implementation def process_payment(self, amount: float, card_no: str) -> bool: ....
Para evitar alucinaciones y limitar el alcance, concéntrese en una pequeña pieza a la vez y asegúrese de que cada clase/función tenga una responsabilidad única y bien definida. Desarrollar de forma incremental para tener más control sobre el código que se genera.
class UserManager: # user creation logic def create_user(self, username, email): ... class EmailService: # send welcome email logic def send_welcome_email(self, email): .... class NotificationService: # send sms notification def send_sms(self, username, email): ... # Usage user_manager = UserManager() email_svc = EmailService() user = user_manager.create_user("hacker", "[email protected]") email_svc.send_welcome_email("[email protected]")
Para simplificar la descripción de la estructura del código a los LLM y comprender sus sugerencias, utilice reglas de nomenclatura claras y coherentes.
# classes: PascalCase class UserAccount: pass # functions and variables: snake_case def calculate_total_price(item_price, quantity): total_cost = item_price * quantity return total_cost # constants: UPPERCASE_WITH_UNDERSCORES MAX_LOGIN_ATTEMPTS = 3 # private methods/variables: prefix with underscore class DatabaseConnection: def __init__(self): self._connection = None def _connect_to_database(self): pass
Para generar implementaciones específicas basadas en los requisitos, cree código esqueleto para estructuras de código comunes y utilícelos como código inicial.
# Todo item - pydantic Model class TodoItem(BaseModel): id: Optional[int] = None title: str description: Optional[str] = None completed: bool = False todo_db = [<insert mock data>] @app.get("/todos", response_model=List[TodoItem]) async def get_todos(): # TODO: retrieve all todos @app.get("/todos/{todo_id}", response_model=TodoItem) async def get_todo(todo_id: int): # TODO: retrieve specific todo @app.post("/todos", response_model=TodoItem) async def create_todo(todo: TodoItem): # TODO: create todo @app.put("/todos/{todo_id}", response_model=TodoItem) async def update_todo(todo_id: int, todo: TodoItem): # TODO: update todo @app.delete("/todos/{todo_id}") async def delete_todo(todo_id: int): # TODO: delete todo
Para ayudar a guiar el LLM, escriba documentación concisa para cada componente antes de la implementación. Además, genere cadenas de documentación que puedan guiar las indicaciones en el futuro utilizando el propio LLM.
inmediato:
Use a Documentation Driven Development approach to write clear, concise docstrings for the following python function calculate_discount(price, discount_percentage) takes in - price (float), original price of the item. - discount_percentage (float), discount percentage range(0-100). returns - price after discount (float) raises - ValueError, when discount_percentage is out of range example - calculate_discount(100, 10) -> 10.0
respuesta:
def calculate_discount(price: float, discount_percent: float) -> float: """ Calculate the discounted price of an item. Args: price (float): original price for the item. discount_percentage (float): discount percentage range(0-100). Returns: float: The price after applying the discount. Raises: ValueError: If discount_percentage is not between 0 and 100. Example: >>> calculate_discount(100, 20) 80.0 """ if not 0 <= discount_percent <= 100: raise ValueError("Discount percent must be between 0 and 100") discount_amount = price * (discount_percent / 100) return price - discount_amount
Para ayudar a los LLM con la implementación y, en particular, a manejar casos extremos, escriba pruebas usted mismo o utilice LLM para hacerlo antes de implementar la función real. Los LLM no ejecutan código internamente, pero realizar pruebas de antemano para garantizar que el código generado cumpla con los requisitos de la función puede ser útil para detectar errores en el código generado por LLM.
caso de prueba:
def is_palindrome(s: str) -> bool: """check if string is palindrome.""" return s == s[::-1] class TestPalindrome(unittest.TestCase): def test_palindrome(self): self.assertTrue(is_palindrome("racecar")) self.assertTrue(is_palindrome("A man a plan a canal Panama")) self.assertFalse(is_palindrome("hello")) self.assertTrue(is_palindrome("")) # Edge case: empty string
inmediato:
given the test case <insert test>, use Document Driven Development to write a function for TestPalindrome
respuesta:
def is_palindrome(s: str) -> bool: """ Check if a string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward, ignoring spaces, punctuation, and capitalization. Args: s (str): The input string to check. Returns: bool: True if the input is a palindrome, False otherwise. Examples: >>> is_palindrome("racecar") True >>> is_palindrome("A man a plan a canal Panama") True >>> is_palindrome("hello") False >>> is_palindrome("") True """ # Remove non-alphanumeric characters and convert to lowercase cleaned = ''.join(char.lower() for char in s if char.isalnum()) # Compare the cleaned string with its reverse return cleaned == cleaned[::-1]
Para discutir múltiples detalles de implementación con el LLM, mantenga una biblioteca de patrones de diseño comúnmente utilizados como referencia.
# Singleton pattern class DatabaseConnection: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) # initialize database connection return cls._instance # Factory pattern class AnimalFactory: @staticmethod def create_animal(animal_type): if animal_type == "dog": return Dog() elif animal_type == "cat": return Cat() else: raise ValueError("Unknown animal type") # Observer pattern class Subject: def __init__(self): self._observers = [] def attach(self, observer): self._observers.append(observer) def detach(self, observer): self._observers.remove(observer) def notify(self): for observer in self._observers: observer.update() # Adapter pattern class LLMAdapter: def __init__(self, llm_service): self.llm_service = llm_service def generate_code(self, prompt): llm_response = self.llm_service.complete(prompt) return self.extract_code(llm_response) def extract_code(self, response): pass
Para garantizar la calidad y la coherencia, cree una lista de verificación para revisar el código generado por LLM.
# Code Review Checklist ## Functionality - [ ] Code performs the intended task correctly - [ ] Edge cases are handled appropriately ## Code Quality - [ ] Code follows project's style guide - [ ] Variable and function names are descriptive and consistent - [ ] No unnecessary comments or dead code ## Performance - [ ] Code is optimized for efficiency - [ ] No potential performance bottlenecks ## Security - [ ] Input validation is implemented - [ ] Sensitive data is handled securely ## Testing - [ ] Unit tests are included and pass - [ ] Edge cases are covered in tests ## Documentation - [ ] Functions and classes are properly documented - [ ] Complex logic is explained in comments
Los LLM funcionan mejor con una estructura definida, así que desarrolle una estrategia para dividir las tareas de codificación en indicaciones más pequeñas. Seguir un enfoque organizado ayuda a generar código de trabajo sin que el LLM vuelva a corregir el código generado varias veces.
inmediato:
I need to implement a function to calculate the Fibonacci number sequence using a Document Driven Development approach. 1. Purpose: function that generates the Fibonacci sequence up to a given number of terms. 2. Interface: def fibonacci_seq(n: int) -> List[int]: """ generate Fibonacci sequence up to n terms. Args: n (int): number of terms in the sequence Returns: List[int]: fibonacci sequence """ 3. Key Functionalities: - handle input validation (n should always be a positive integer) - generate the sequence starting with 0 and 1 - each subsequent number is the sum of two preceding ones - return the sequence as a list 4. Implementation Details: - use a loop to generate the sequence - store the sequence in a list - optimize for memory by only keeping the last two numbers in memory if needed 5. Test Cases: - fibonacci_seq(0) should return [] - fibonacci_seq(1) should return [0] - fibonacci_seq(5) should return [0, 1, 1, 2, 3]
Si bien todos los ejemplos anteriores pueden parecer sencillos, seguir prácticas fundamentales como la arquitectura modular y la ingeniería rápida eficaz, y adoptar un enfoque estructurado sólido en el desarrollo asistido por un LLM, marca una gran diferencia a escala. Al implementar estas prácticas, más desarrolladores pueden maximizar los beneficios del uso de LLM, lo que resulta en una mayor productividad y calidad del código.
Los LLM son herramientas poderosas que funcionan mejor cuando se guían por principios de ingeniería de software que son fáciles de pasar por alto. Interiorizarlos podría marcar la diferencia entre un código elegantemente elaborado y una gran bola de barro generada aleatoriamente.
El objetivo de este artículo es alentar a los desarrolladores a tener siempre en cuenta estas prácticas al utilizar LLM para producir código de alta calidad y ahorrar tiempo en el futuro. A medida que los LLM continúen mejorando, los fundamentos se volverán aún más cruciales para aprovecharlos al máximo.
Para profundizar en los principios de desarrollo de software, consulte este libro de texto clásico: Arquitectura limpia: una guía para artesanos sobre la estructura y el diseño de software de Robert C. Martin.
Si disfrutó de este artículo, permanezca atento al próximo, donde profundizaremos en un flujo de trabajo detallado para el desarrollo asistido por LLM. ¡Comparta cualquier otro concepto que crea que es importante en los comentarios! Gracias.